|
19 | 19 | ) |
20 | 20 | from qiskit.providers import Options |
21 | 21 |
|
22 | | -from qiskit_ionq.helpers import ( |
23 | | - GATESET_MAP, |
24 | | - qiskit_circ_to_ionq_circ, |
| 22 | + |
| 23 | +##################################################################### |
| 24 | +##################################################################### |
| 25 | +##################################################################### |
| 26 | +# Vendored from qiskit_ionq |
| 27 | + |
| 28 | + |
| 29 | +# the qiskit gates that the IonQ backend can serialize to our IR |
| 30 | +# not the actual hardware basis gates for the system — we do our own transpilation pass. |
| 31 | +# also not an exact/complete list of the gates IonQ's backend takes |
| 32 | +# by name — please refer to IonQ docs for that. |
| 33 | +# |
| 34 | +# Some of these gates may be deprecated or removed in qiskit 1.0 |
| 35 | +ionq_basis_gates = [ |
| 36 | + "ccx", |
| 37 | + "ch", |
| 38 | + "cnot", |
| 39 | + "cp", |
| 40 | + "crx", |
| 41 | + "cry", |
| 42 | + "crz", |
| 43 | + "csx", |
| 44 | + "cx", |
| 45 | + "cy", |
| 46 | + "cz", |
| 47 | + "h", |
| 48 | + "i", |
| 49 | + "id", |
| 50 | + "mcp", |
| 51 | + "mcphase", |
| 52 | + "mct", |
| 53 | + "mcx", |
| 54 | + "measure", |
| 55 | + "p", |
| 56 | + "rx", |
| 57 | + "rxx", |
| 58 | + "ry", |
| 59 | + "ryy", |
| 60 | + "rz", |
| 61 | + "rzz", |
| 62 | + "s", |
| 63 | + "sdg", |
| 64 | + "swap", |
| 65 | + "sx", |
| 66 | + "sxdg", |
| 67 | + "t", |
| 68 | + "tdg", |
| 69 | + "toffoli", |
| 70 | + "x", |
| 71 | + "y", |
| 72 | + "z", |
| 73 | + "PauliEvolution", |
| 74 | +] |
| 75 | + |
| 76 | +# https://ionq.com/docs/getting-started-with-native-gates |
| 77 | +ionq_native_basis_gates = [ |
| 78 | + "gpi", |
| 79 | + "gpi2", |
| 80 | + "ms", # Pairwise MS gate |
| 81 | + "zz", # ZZ gate |
| 82 | +] |
| 83 | + |
| 84 | +# Each language corresponds to a different set of basis gates. |
| 85 | +GATESET_MAP = { |
| 86 | + "qis": ionq_basis_gates, |
| 87 | + "native": ionq_native_basis_gates, |
| 88 | +} |
| 89 | + |
| 90 | +ionq_api_aliases = { # todo fix alias bug |
| 91 | + "cp": "cz", |
| 92 | + "csx": "cv", |
| 93 | + "mcphase": "cz", |
| 94 | + "ccx": "cx", # just one C for all mcx |
| 95 | + "mcx": "cx", # just one C for all mcx |
| 96 | + "tdg": "ti", |
| 97 | + "p": "z", |
| 98 | + "PauliEvolution": "pauliexp", |
| 99 | + "rxx": "xx", |
| 100 | + "ryy": "yy", |
| 101 | + "rzz": "zz", |
| 102 | + "sdg": "si", |
| 103 | + "sx": "v", |
| 104 | + "sxdg": "vi", |
| 105 | +} |
| 106 | + |
| 107 | +from qiskit.circuit import ( |
| 108 | + controlledgate as q_cgates, |
| 109 | + QuantumCircuit, |
25 | 110 | ) |
26 | 111 |
|
| 112 | +from typing import Literal, Any |
| 113 | + |
| 114 | +from qiskit.exceptions import QiskitError |
| 115 | + |
| 116 | +class IonQError(QiskitError): |
| 117 | + """Base class for errors raised by an IonQProvider.""" |
| 118 | + |
| 119 | + def __str__(self) -> str: |
| 120 | + return f"{self.__class__.__name__}({self.message!r})" |
| 121 | + |
| 122 | + def __repr__(self) -> str: |
| 123 | + return repr(str(self)) |
| 124 | + |
| 125 | +class JobError(QiskitError): |
| 126 | + """Base class for errors raised by Jobs.""" |
| 127 | + |
| 128 | + pass |
| 129 | + |
| 130 | +class IonQGateError(IonQError, JobError): |
| 131 | + """Errors generated from invalid gate defs |
| 132 | +
|
| 133 | + Attributes: |
| 134 | + gate_name: The name of the gate which caused this error. |
| 135 | + """ |
| 136 | + |
| 137 | + def __init__(self, gate_name: str, gateset: Literal["qis", "native"]): |
| 138 | + self.gate_name = gate_name |
| 139 | + self.gateset = gateset |
| 140 | + super().__init__( |
| 141 | + ( |
| 142 | + f"gate '{gate_name}' is not supported on the '{gateset}' IonQ backends. " |
| 143 | + "Please use the qiskit.transpile method, manually rewrite to remove the gate, " |
| 144 | + "or change the gateset selection as appropriate." |
| 145 | + ) |
| 146 | + ) |
| 147 | + |
| 148 | + def __repr__(self): |
| 149 | + return f"{self.__class__.__name__}(gate_name={self.gate_name!r}, gateset={self.gateset!r})" |
| 150 | + |
| 151 | +class IonQMidCircuitMeasurementError(IonQError, JobError): |
| 152 | + """Errors generated from attempting mid-circuit measurement, which is not supported. |
| 153 | + Measurement must come after all instructions. |
| 154 | +
|
| 155 | + Attributes: |
| 156 | + qubit_index: The qubit index to be measured mid-circuit |
| 157 | + """ |
| 158 | + |
| 159 | + def __init__(self, qubit_index: int, gate_name: str): |
| 160 | + self.qubit_index = qubit_index |
| 161 | + self.gate_name = gate_name |
| 162 | + super().__init__( |
| 163 | + f"Attempting to put '{gate_name}' after a measurement on qubit {qubit_index}. " |
| 164 | + "Mid-circuit measurement is not supported." |
| 165 | + ) |
| 166 | + |
| 167 | + def __str__(self): |
| 168 | + kwargs = f"qubit_index={self.qubit_index!r}, gate_name={self.gate_name!r}" |
| 169 | + return f"{self.__class__.__name__}({kwargs})" |
| 170 | + |
| 171 | +class IonQPauliExponentialError(IonQError): |
| 172 | + """Errors generated from improper usage of Pauli exponentials.""" |
| 173 | + |
| 174 | +def paulis_commute(pauli_terms: list[str]) -> bool: |
| 175 | + """Check if a list of Pauli terms commute. |
| 176 | +
|
| 177 | + Args: |
| 178 | + pauli_terms (list): A list of Pauli terms. |
| 179 | +
|
| 180 | + Returns: |
| 181 | + bool: Whether the Pauli terms commute. |
| 182 | + """ |
| 183 | + for i, term in enumerate(pauli_terms): |
| 184 | + for other_term in pauli_terms[i:]: |
| 185 | + assert len(term) == len(other_term) |
| 186 | + anticommutation_parity = 0 |
| 187 | + for index, char in enumerate(term): |
| 188 | + other_char = other_term[index] |
| 189 | + if "I" not in (char, other_char): |
| 190 | + if char != other_char: |
| 191 | + anticommutation_parity += 1 |
| 192 | + if anticommutation_parity % 2 == 1: |
| 193 | + return False |
| 194 | + return True |
| 195 | + |
| 196 | +def qiskit_circ_to_ionq_circ( |
| 197 | + input_circuit: QuantumCircuit, |
| 198 | + gateset: Literal["qis", "native"] = "qis", |
| 199 | + ionq_compiler_synthesis: bool = False, |
| 200 | +): |
| 201 | + """Build a circuit in IonQ's instruction format from qiskit instructions. |
| 202 | +
|
| 203 | + .. ATTENTION:: This function ignores the following compiler directives: |
| 204 | + * ``barrier`` |
| 205 | +
|
| 206 | + Parameters: |
| 207 | + input_circuit (:class:`qiskit.circuit.QuantumCircuit`): A Qiskit quantum circuit. |
| 208 | + gateset (string): Set of gates to target. It can be QIS (required transpilation pass in |
| 209 | + IonQ backend, which is sent standard gates) or native (only IonQ native gates are |
| 210 | + allowed, in the future we may provide transpilation to these gates in Qiskit). |
| 211 | + ionq_compiler_synthesis (bool): Whether to opt-in to IonQ compiler's intelligent |
| 212 | + trotterization. |
| 213 | +
|
| 214 | + Raises: |
| 215 | + IonQGateError: If an unsupported instruction is supplied. |
| 216 | + IonQMidCircuitMeasurementError: If a mid-circuit measurement is detected. |
| 217 | + IonQPauliExponentialError: If non-commuting PauliExponentials are found without |
| 218 | + the appropriate flag. |
| 219 | +
|
| 220 | + Returns: |
| 221 | + list[dict]: A list of instructions in a converted dict format. |
| 222 | + int: The number of measurements. |
| 223 | + dict: The measurement map from qubit number to classical bit number. |
| 224 | + """ |
| 225 | + compiler_directives = ["barrier"] |
| 226 | + output_circuit = [] |
| 227 | + num_meas = 0 |
| 228 | + meas_map = [None] * len(input_circuit.clbits) |
| 229 | + for inst in input_circuit.data: |
| 230 | + instruction, qargs, cargs = inst.operation, inst.qubits, inst.clbits |
| 231 | + |
| 232 | + # Don't process compiler directives. |
| 233 | + instruction_name = instruction.name |
| 234 | + if instruction_name in compiler_directives: |
| 235 | + continue |
| 236 | + |
| 237 | + # Don't process measurement instructions. |
| 238 | + if instruction_name == "measure": |
| 239 | + meas_map[input_circuit.clbits.index(cargs[0])] = input_circuit.qubits.index( |
| 240 | + qargs[0] |
| 241 | + ) |
| 242 | + num_meas += 1 |
| 243 | + continue |
| 244 | + |
| 245 | + # serialized identity gate is a no-op |
| 246 | + if instruction_name == "id": |
| 247 | + continue |
| 248 | + |
| 249 | + # Raise out for instructions we don't support. |
| 250 | + if instruction_name not in GATESET_MAP[gateset]: |
| 251 | + raise IonQGateError(instruction_name, gateset) |
| 252 | + |
| 253 | + # Process the instruction and convert. |
| 254 | + rotation: dict[str, Any] = {} |
| 255 | + if len(instruction.params) > 0: |
| 256 | + if gateset == "qis" or ( |
| 257 | + len(instruction.params) == 1 and instruction_name != "zz" |
| 258 | + ): |
| 259 | + # The float is here to cast Qiskit ParameterExpressions to numbers |
| 260 | + rotation = { |
| 261 | + ("rotation" if gateset == "qis" else "phase"): float( |
| 262 | + instruction.params[0] |
| 263 | + ) |
| 264 | + } |
| 265 | + if instruction_name == "PauliEvolution": |
| 266 | + # rename rotation to time |
| 267 | + rotation["time"] = rotation.pop("rotation") |
| 268 | + elif instruction_name in {"zz"}: |
| 269 | + rotation = {"angle": instruction.params[0]} |
| 270 | + else: |
| 271 | + rotation = { |
| 272 | + "phases": [float(t) for t in instruction.params[:2]], |
| 273 | + "angle": instruction.params[2], |
| 274 | + } |
| 275 | + |
| 276 | + # Default conversion is simple, just gate & target(s). |
| 277 | + targets = [input_circuit.qubits.index(qargs[0])] |
| 278 | + if instruction_name in {"ms", "zz"}: |
| 279 | + targets.append(input_circuit.qubits.index(qargs[1])) |
| 280 | + |
| 281 | + converted = ( |
| 282 | + {"gate": instruction_name, "targets": targets} |
| 283 | + if instruction_name not in {"gpi", "gpi2"} |
| 284 | + else { |
| 285 | + "gate": instruction_name, |
| 286 | + "target": targets[0], |
| 287 | + } |
| 288 | + ) |
| 289 | + |
| 290 | + # re-alias certain names |
| 291 | + if instruction_name in ionq_api_aliases: |
| 292 | + instruction_name = ionq_api_aliases[instruction_name] |
| 293 | + converted["gate"] = instruction_name |
| 294 | + |
| 295 | + # Make sure uncontrolled multi-targets use all qargs. |
| 296 | + if instruction.num_qubits > 1 and not hasattr(instruction, "num_ctrl_qubits"): |
| 297 | + converted["targets"] = [ |
| 298 | + input_circuit.qubits.index(qargs[i]) |
| 299 | + for i in range(instruction.num_qubits) |
| 300 | + ] |
| 301 | + |
| 302 | + # If this is a controlled gate, make sure to set control qubits. |
| 303 | + if isinstance(instruction, q_cgates.ControlledGate): |
| 304 | + gate = instruction_name[1:] # trim the leading c |
| 305 | + controls = [input_circuit.qubits.index(qargs[0])] |
| 306 | + targets = [input_circuit.qubits.index(qargs[1])] |
| 307 | + # If this is a multi-control, use more than one qubit. |
| 308 | + if instruction.num_ctrl_qubits > 1: |
| 309 | + controls = [ |
| 310 | + input_circuit.qubits.index(qargs[i]) |
| 311 | + for i in range(instruction.num_ctrl_qubits) |
| 312 | + ] |
| 313 | + targets = [ |
| 314 | + input_circuit.qubits.index(qargs[instruction.num_ctrl_qubits]) |
| 315 | + ] |
| 316 | + if gate == "swap": |
| 317 | + # If this is a cswap, we have two targets: |
| 318 | + targets = [ |
| 319 | + input_circuit.qubits.index(qargs[-2]), |
| 320 | + input_circuit.qubits.index(qargs[-1]), |
| 321 | + ] |
| 322 | + |
| 323 | + # Update converted gate values. |
| 324 | + converted.update( |
| 325 | + { |
| 326 | + "gate": gate, |
| 327 | + "controls": controls, |
| 328 | + "targets": targets, |
| 329 | + } |
| 330 | + ) |
| 331 | + |
| 332 | + if instruction_name == "pauliexp": |
| 333 | + imag_coeff = any(coeff.imag for coeff in instruction.operator.coeffs) |
| 334 | + assert not imag_coeff, ( |
| 335 | + "PauliEvolution gate must have real coefficients, " |
| 336 | + f"but got {imag_coeff}" |
| 337 | + ) |
| 338 | + terms = [term[0] for term in instruction.operator.to_list()] |
| 339 | + if not ionq_compiler_synthesis and not paulis_commute(terms): |
| 340 | + raise IonQPauliExponentialError( |
| 341 | + f"You have included a PauliEvolutionGate with non-commuting terms: {terms}." |
| 342 | + "To decompose it with IonQ hardware-aware synthesis, resubmit with the " |
| 343 | + "IONQ_COMPILER_SYNTHESIS flag." |
| 344 | + ) |
| 345 | + targets = [ |
| 346 | + input_circuit.qubits.index(qargs[i]) |
| 347 | + for i in range(instruction.num_qubits) |
| 348 | + ] |
| 349 | + coefficients = [coeff.real for coeff in instruction.operator.coeffs] |
| 350 | + gate = { |
| 351 | + "gate": instruction_name, |
| 352 | + "targets": targets, |
| 353 | + "terms": terms, |
| 354 | + "coefficients": coefficients, |
| 355 | + } |
| 356 | + converted.update(gate) |
| 357 | + |
| 358 | + # if there's a valid instruction after a measurement, |
| 359 | + if num_meas > 0: |
| 360 | + # see if any of the involved qubits have been measured, |
| 361 | + # and raise if so — no mid-circuit measurement! |
| 362 | + controls_and_targets = converted.get("targets", []) + converted.get( |
| 363 | + "controls", [] |
| 364 | + ) |
| 365 | + if any(i in meas_map for i in controls_and_targets): |
| 366 | + raise IonQMidCircuitMeasurementError( |
| 367 | + input_circuit.qubits.index(qargs[0]), instruction_name |
| 368 | + ) |
| 369 | + |
| 370 | + output_circuit.append({**converted, **rotation}) |
| 371 | + |
| 372 | + return output_circuit, num_meas, meas_map |
| 373 | + |
| 374 | +##################################################################### |
| 375 | +##################################################################### |
| 376 | +##################################################################### |
| 377 | + |
27 | 378 | if TYPE_CHECKING: |
28 | 379 | from azure.quantum.qiskit import AzureQuantumProvider |
29 | 380 |
|
|
0 commit comments