microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
source/pip/qsharp/interop/cirq/_neutral_atom.py
166lines · modecode
| 1 | # Copyright (c) Microsoft Corporation. |
| 2 | # Licensed under the MIT License. |
| 3 | |
| 4 | """NeutralAtomSampler — a cirq.Sampler backed by the local NeutralAtomDevice.""" |
| 5 | |
| 6 | from __future__ import annotations |
| 7 | |
| 8 | from typing import List, Optional, TYPE_CHECKING |
| 9 | |
| 10 | import cirq |
| 11 | |
| 12 | from ._result import NeutralAtomCirqResult, measurement_dict, to_cirq_result |
| 13 | |
| 14 | if TYPE_CHECKING: |
| 15 | from qsharp._simulation import NoiseConfig |
| 16 | from qsharp._device._atom import NeutralAtomDevice |
| 17 | |
| 18 | |
| 19 | class NeutralAtomSampler(cirq.Sampler): |
| 20 | """A ``cirq.Sampler`` that runs Cirq circuits on the local NeutralAtomDevice simulator. |
| 21 | |
| 22 | This sampler integrates with the standard Cirq sampler protocol, so it can |
| 23 | be used anywhere a ``cirq.Sampler`` is expected. |
| 24 | |
| 25 | Pipeline for each ``run()`` call: |
| 26 | |
| 27 | 1. ``cirq.Circuit.to_qasm(version="3.0")`` → OpenQASM 3.0 |
| 28 | 2. OpenQASM 3.0 → QIR (base profile, via the Q# compiler) |
| 29 | 3. QIR → ``NeutralAtomDevice.simulate()`` (decompose, schedule, simulate) |
| 30 | 4. Raw shots → :class:`NeutralAtomCirqResult` |
| 31 | |
| 32 | Args: |
| 33 | noise: Optional :class:`~qsharp._simulation.NoiseConfig` describing |
| 34 | per-gate noise. The device decomposes gates to the native set |
| 35 | ``{Rz, SX, CZ, MResetZ}``; configure noise on those native gates. |
| 36 | For example, a Cirq ``X`` gate arriving via QASM 2.0 is decomposed |
| 37 | to ``SX·SX``, so ``noise.sx`` is the relevant field. Defaults to |
| 38 | ``None`` (noiseless). |
| 39 | simulator_type: Force a particular simulator backend. |
| 40 | - ``"clifford"`` — Clifford-only, fast. Requires a Clifford circuit. |
| 41 | - ``"cpu"`` — Full state-vector on CPU. |
| 42 | - ``"gpu"`` — Full state-vector on GPU. |
| 43 | - ``None`` (default) — GPU if available, CPU otherwise. |
| 44 | seed: Optional integer seed for reproducibility. Defaults to ``None``. |
| 45 | device: An existing :class:`~qsharp._device._atom.NeutralAtomDevice` |
| 46 | instance to reuse across calls. A default-configured device is |
| 47 | created lazily on the first call when not provided. |
| 48 | |
| 49 | Example:: |
| 50 | |
| 51 | import cirq |
| 52 | from qsharp.interop.cirq import NeutralAtomSampler |
| 53 | from qsharp._simulation import NoiseConfig |
| 54 | |
| 55 | q0, q1 = cirq.LineQubit.range(2) |
| 56 | circuit = cirq.Circuit([ |
| 57 | cirq.H(q0), |
| 58 | cirq.CNOT(q0, q1), |
| 59 | cirq.measure(q0, q1, key="m"), |
| 60 | ]) |
| 61 | |
| 62 | # Noiseless simulation |
| 63 | sampler = NeutralAtomSampler(seed=42) |
| 64 | result = sampler.run(circuit, repetitions=1000) |
| 65 | print(result.histogram(key="m")) |
| 66 | |
| 67 | # Noisy simulation — 1% loss on Rz (native gate) |
| 68 | noise = NoiseConfig() |
| 69 | noise.rz.loss = 0.01 |
| 70 | sampler = NeutralAtomSampler(noise=noise, seed=42) |
| 71 | result = sampler.run(circuit, repetitions=1000) |
| 72 | print(f"Accepted: {len(result.measurements['m'])} / {len(result.raw_shots)}") |
| 73 | """ |
| 74 | |
| 75 | def __init__( |
| 76 | self, |
| 77 | *, |
| 78 | noise: Optional["NoiseConfig"] = None, |
| 79 | simulator_type: Optional[str] = None, |
| 80 | seed: Optional[int] = None, |
| 81 | device: Optional["NeutralAtomDevice"] = None, |
| 82 | ) -> None: |
| 83 | self._noise = noise |
| 84 | self._simulator_type = simulator_type |
| 85 | self._seed = seed |
| 86 | self._device = device |
| 87 | |
| 88 | def _get_device(self) -> "NeutralAtomDevice": |
| 89 | """Return the NeutralAtomDevice, creating a default one on first access.""" |
| 90 | if self._device is None: |
| 91 | from qsharp._device._atom import NeutralAtomDevice |
| 92 | |
| 93 | self._device = NeutralAtomDevice() |
| 94 | return self._device |
| 95 | |
| 96 | def run_sweep( |
| 97 | self, |
| 98 | program: cirq.AbstractCircuit, |
| 99 | params: cirq.Sweepable, |
| 100 | repetitions: int = 1, |
| 101 | ) -> List[NeutralAtomCirqResult]: |
| 102 | """Run the circuit for each parameter resolver in the sweep. |
| 103 | |
| 104 | Args: |
| 105 | program: The Cirq circuit to simulate. |
| 106 | params: A :class:`cirq.Sweepable` defining the parameter resolvers |
| 107 | to sweep over. Each resolver produces one result. |
| 108 | repetitions: Number of shots per parameter resolver. |
| 109 | |
| 110 | Returns: |
| 111 | A list of :class:`NeutralAtomCirqResult` objects, one per resolver. |
| 112 | """ |
| 113 | resolvers = list(cirq.to_sweep(params)) if params is not None else [cirq.ParamResolver()] |
| 114 | return [ |
| 115 | self._run_once(program, resolver, repetitions) for resolver in resolvers |
| 116 | ] |
| 117 | |
| 118 | def _run_once( |
| 119 | self, |
| 120 | circuit: cirq.AbstractCircuit, |
| 121 | param_resolver: cirq.ParamResolver, |
| 122 | repetitions: int, |
| 123 | ) -> NeutralAtomCirqResult: |
| 124 | from qsharp._native import compile_qasm_program_to_qir |
| 125 | from qsharp._fs import read_file, list_directory, resolve |
| 126 | from qsharp._http import fetch_github |
| 127 | from qsharp._qsharp import TargetProfile |
| 128 | |
| 129 | # Resolve parameters |
| 130 | resolved_circuit = cirq.resolve_parameters(circuit, param_resolver) |
| 131 | |
| 132 | # Step 1: Cirq circuit → QASM 3.0 |
| 133 | try: |
| 134 | qasm = resolved_circuit.to_qasm(version="3.0") |
| 135 | except Exception as exc: |
| 136 | raise ValueError( |
| 137 | "Failed to convert the Cirq circuit to QASM 3.0. " |
| 138 | "Ensure every gate in the circuit supports QASM serialization " |
| 139 | f"(see cirq.Circuit.to_qasm). Original error: {exc}" |
| 140 | ) from exc |
| 141 | |
| 142 | # Step 2: QASM 3.0 → QIR (base profile) |
| 143 | qir = compile_qasm_program_to_qir( |
| 144 | qasm, |
| 145 | read_file, |
| 146 | list_directory, |
| 147 | resolve, |
| 148 | fetch_github, |
| 149 | name="cirq_circuit", |
| 150 | target_profile=TargetProfile.Base, |
| 151 | search_path=".", |
| 152 | ) |
| 153 | |
| 154 | # Step 3: QIR → NeutralAtomDevice simulation |
| 155 | device = self._get_device() |
| 156 | raw_shots = device.simulate( |
| 157 | qir, |
| 158 | shots=repetitions, |
| 159 | noise=self._noise, |
| 160 | type=self._simulator_type, |
| 161 | seed=self._seed, |
| 162 | ) |
| 163 | |
| 164 | # Step 4: Build NeutralAtomCirqResult |
| 165 | meas_dict = measurement_dict(resolved_circuit) |
| 166 | return to_cirq_result(raw_shots, meas_dict, param_resolver) |