|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
| 16 | +"""Contain a backend that saves the unitary of a quantum circuit.""" |
| 17 | + |
| 18 | +from copy import deepcopy |
| 19 | +import itertools |
| 20 | +import math |
| 21 | +import warnings |
| 22 | +import random |
| 23 | +import numpy as np |
| 24 | + |
| 25 | +from projectq.cengines import BasicEngine |
| 26 | +from projectq.types import WeakQubitRef |
| 27 | +from projectq.meta import has_negative_control, get_control_count, LogicalQubitIDTag |
| 28 | +from projectq.ops import ( |
| 29 | + AllocateQubitGate, |
| 30 | + DeallocateQubitGate, |
| 31 | + MeasureGate, |
| 32 | + FlushGate, |
| 33 | +) |
| 34 | + |
| 35 | + |
| 36 | +def _qidmask(target_ids, control_ids, n_qubits): |
| 37 | + """ |
| 38 | + Calculate index masks. |
| 39 | +
|
| 40 | + Args: |
| 41 | + target_ids (list): list of target qubit indices |
| 42 | + control_ids (list): list of control qubit indices |
| 43 | + control_state (list): list of states for the control qubits (0 or 1) |
| 44 | + n_qubits (int): number of qubits |
| 45 | + """ |
| 46 | + mask_list = [] |
| 47 | + perms = np.array([x[::-1] for x in itertools.product("01", repeat=n_qubits)]).astype(int) |
| 48 | + all_ids = np.array(range(n_qubits)) |
| 49 | + irel_ids = np.delete(all_ids, control_ids + target_ids) |
| 50 | + |
| 51 | + if len(control_ids) > 0: |
| 52 | + cmask = np.where(np.all(perms[:, control_ids] == [1] * len(control_ids), axis=1)) |
| 53 | + else: |
| 54 | + cmask = np.array(range(perms.shape[0])) |
| 55 | + |
| 56 | + if len(irel_ids) > 0: |
| 57 | + irel_perms = np.array([x[::-1] for x in itertools.product("01", repeat=len(irel_ids))]).astype(int) |
| 58 | + for i in range(2 ** len(irel_ids)): |
| 59 | + irel_mask = np.where(np.all(perms[:, irel_ids] == irel_perms[i], axis=1)) |
| 60 | + common = np.intersect1d(irel_mask, cmask) |
| 61 | + if len(common) > 0: |
| 62 | + mask_list.append(common) |
| 63 | + else: |
| 64 | + irel_mask = np.array(range(perms.shape[0])) |
| 65 | + mask_list.append(np.intersect1d(irel_mask, cmask)) |
| 66 | + return mask_list |
| 67 | + |
| 68 | + |
| 69 | +class UnitarySimulator(BasicEngine): |
| 70 | + """ |
| 71 | + Simulator engine aimed at calculating the unitary transformation that represents the current quantum circuit. |
| 72 | +
|
| 73 | + Attributes: |
| 74 | + unitary (np.ndarray): Current unitary representing the quantum circuit being processed so far. |
| 75 | + history (list<np.ndarray>): List of previous quantum circuit unitaries. |
| 76 | +
|
| 77 | + Note: |
| 78 | + The current implementation of this backend resets the unitary after the first gate that is neither a qubit |
| 79 | + deallocation nor a measurement occurs after one of those two aforementioned gates. |
| 80 | +
|
| 81 | + The old unitary call be accessed at anytime after such a situation occurs via the `history` property. |
| 82 | +
|
| 83 | + .. code-block:: python |
| 84 | +
|
| 85 | + eng = MainEngine(backend=UnitarySimulator(), engine_list=[]) |
| 86 | + qureg = eng.allocate_qureg(3) |
| 87 | + All(X) | qureg |
| 88 | +
|
| 89 | + eng.flush() |
| 90 | + All(Measure) | qureg |
| 91 | + eng.deallocate_qubit(qureg[1]) |
| 92 | +
|
| 93 | + X | qureg[0] # WARNING: appending gate after measurements or deallocations resets the unitary |
| 94 | + """ |
| 95 | + |
| 96 | + def __init__(self): |
| 97 | + """Initialize a UnitarySimulator object.""" |
| 98 | + super().__init__() |
| 99 | + self._qubit_map = dict() |
| 100 | + self._unitary = [1] |
| 101 | + self._num_qubits = 0 |
| 102 | + self._is_valid = True |
| 103 | + self._is_flushed = False |
| 104 | + self._state = [1] |
| 105 | + self._history = [] |
| 106 | + |
| 107 | + @property |
| 108 | + def unitary(self): |
| 109 | + """ |
| 110 | + Access the last unitary matrix directly. |
| 111 | +
|
| 112 | + Returns: |
| 113 | + A numpy array which is the unitary matrix of the circuit. |
| 114 | + """ |
| 115 | + return deepcopy(self._unitary) |
| 116 | + |
| 117 | + @property |
| 118 | + def history(self): |
| 119 | + """ |
| 120 | + Access all previous unitary matrices. |
| 121 | +
|
| 122 | + The current unitary matrix is appended to this list once a gate is received after either a measurement or a |
| 123 | + qubit deallocation has occurred. |
| 124 | +
|
| 125 | + Returns: |
| 126 | + A list where the elements are all previous unitary matrices representing the circuit, separated by |
| 127 | + measurement/deallocate gates. |
| 128 | + """ |
| 129 | + return deepcopy(self._history) |
| 130 | + |
| 131 | + def is_available(self, cmd): |
| 132 | + """ |
| 133 | + Test whether a Command is supported by a compiler engine. |
| 134 | +
|
| 135 | + Specialized implementation of is_available: The unitary simulator can deal with all arbitrarily-controlled gates |
| 136 | + which provide a gate-matrix (via gate.matrix). |
| 137 | +
|
| 138 | + Args: |
| 139 | + cmd (Command): Command for which to check availability (single- qubit gate, arbitrary controls) |
| 140 | +
|
| 141 | + Returns: |
| 142 | + True if it can be simulated and False otherwise. |
| 143 | + """ |
| 144 | + if has_negative_control(cmd): |
| 145 | + return False |
| 146 | + |
| 147 | + if isinstance(cmd.gate, (AllocateQubitGate, DeallocateQubitGate, MeasureGate)): |
| 148 | + return True |
| 149 | + |
| 150 | + try: |
| 151 | + gate_mat = cmd.gate.matrix |
| 152 | + if len(gate_mat) > 2 ** 6: |
| 153 | + warnings.warn("Potentially large matrix gate encountered! ({} qubits)".format(math.log2(len(gate_mat)))) |
| 154 | + return True |
| 155 | + except AttributeError: |
| 156 | + return False |
| 157 | + |
| 158 | + def receive(self, command_list): |
| 159 | + """ |
| 160 | + Receive a list of commands. |
| 161 | +
|
| 162 | + Receive a list of commands from the previous engine and handle them: |
| 163 | + * update the unitary of the quantum circuit |
| 164 | + * update the internal quantum state if a measurement or a qubit deallocation occurs |
| 165 | +
|
| 166 | + prior to sending them on to the next engine. |
| 167 | +
|
| 168 | + Args: |
| 169 | + command_list (list<Command>): List of commands to execute on the simulator. |
| 170 | + """ |
| 171 | + for cmd in command_list: |
| 172 | + self._handle(cmd) |
| 173 | + |
| 174 | + if not self.is_last_engine: |
| 175 | + self.send(command_list) |
| 176 | + |
| 177 | + def _flush(self): |
| 178 | + """Flush the simulator state.""" |
| 179 | + if not self._is_flushed: |
| 180 | + self._is_flushed = True |
| 181 | + self._state = self._unitary @ self._state |
| 182 | + |
| 183 | + def _handle(self, cmd): |
| 184 | + """ |
| 185 | + Handle all commands. |
| 186 | +
|
| 187 | + Args: |
| 188 | + cmd (Command): Command to handle. |
| 189 | +
|
| 190 | + Raises: |
| 191 | + RuntimeError: If a measurement is performed before flush gate. |
| 192 | + """ |
| 193 | + if isinstance(cmd.gate, AllocateQubitGate): |
| 194 | + self._qubit_map[cmd.qubits[0][0].id] = self._num_qubits |
| 195 | + self._num_qubits += 1 |
| 196 | + self._unitary = np.kron(np.identity(2), self._unitary) |
| 197 | + self._state.extend([0] * len(self._state)) |
| 198 | + |
| 199 | + elif isinstance(cmd.gate, DeallocateQubitGate): |
| 200 | + pos = self._qubit_map[cmd.qubits[0][0].id] |
| 201 | + self._qubit_map = {key: value - 1 if value > pos else value for key, value in self._qubit_map.items()} |
| 202 | + self._num_qubits -= 1 |
| 203 | + self._is_valid = False |
| 204 | + |
| 205 | + elif isinstance(cmd.gate, MeasureGate): |
| 206 | + self._is_valid = False |
| 207 | + |
| 208 | + if not self._is_flushed: |
| 209 | + raise RuntimeError( |
| 210 | + 'Please make sure all previous gates are flushed before measurement so the state gets updated' |
| 211 | + ) |
| 212 | + |
| 213 | + if get_control_count(cmd) != 0: |
| 214 | + raise ValueError('Cannot have control qubits with a measurement gate!') |
| 215 | + |
| 216 | + all_qubits = [qb for qr in cmd.qubits for qb in qr] |
| 217 | + measurements = self.measure_qubits([qb.id for qb in all_qubits]) |
| 218 | + |
| 219 | + for qb, res in zip(all_qubits, measurements): |
| 220 | + # Check if a mapper assigned a different logical id |
| 221 | + for tag in cmd.tags: |
| 222 | + if isinstance(tag, LogicalQubitIDTag): |
| 223 | + qb = WeakQubitRef(qb.engine, tag.logical_qubit_id) |
| 224 | + break |
| 225 | + self.main_engine.set_measurement_result(qb, res) |
| 226 | + |
| 227 | + elif isinstance(cmd.gate, FlushGate): |
| 228 | + self._flush() |
| 229 | + else: |
| 230 | + if not self._is_valid: |
| 231 | + self._flush() |
| 232 | + |
| 233 | + warnings.warn( |
| 234 | + "Processing of other gates after a qubit deallocation or measurement will reset the unitary," |
| 235 | + "previous unitary can be accessed in history" |
| 236 | + ) |
| 237 | + self._history.append(self._unitary) |
| 238 | + self._unitary = np.identity(2 ** self._num_qubits, dtype=complex) |
| 239 | + self._state = np.array([1] + ([0] * (2 ** self._num_qubits - 1)), dtype=complex) |
| 240 | + self._is_valid = True |
| 241 | + |
| 242 | + self._is_flushed = False |
| 243 | + mask_list = _qidmask( |
| 244 | + [self._qubit_map[qb.id] for qr in cmd.qubits for qb in qr], |
| 245 | + [self._qubit_map[qb.id] for qb in cmd.control_qubits], |
| 246 | + self._num_qubits, |
| 247 | + ) |
| 248 | + for mask in mask_list: |
| 249 | + cache = np.identity(2 ** self._num_qubits, dtype=complex) |
| 250 | + cache[np.ix_(mask, mask)] = cmd.gate.matrix |
| 251 | + self._unitary = cache @ self._unitary |
| 252 | + |
| 253 | + def measure_qubits(self, ids): |
| 254 | + """ |
| 255 | + Measure the qubits with IDs ids and return a list of measurement outcomes (True/False). |
| 256 | +
|
| 257 | + Args: |
| 258 | + ids (list<int>): List of qubit IDs to measure. |
| 259 | +
|
| 260 | + Returns: |
| 261 | + List of measurement results (containing either True or False). |
| 262 | + """ |
| 263 | + random_outcome = random.random() |
| 264 | + val = 0.0 |
| 265 | + i_picked = 0 |
| 266 | + while val < random_outcome and i_picked < len(self._state): |
| 267 | + val += np.abs(self._state[i_picked]) ** 2 |
| 268 | + i_picked += 1 |
| 269 | + |
| 270 | + i_picked -= 1 |
| 271 | + |
| 272 | + pos = [self._qubit_map[ID] for ID in ids] |
| 273 | + res = [False] * len(pos) |
| 274 | + |
| 275 | + mask = 0 |
| 276 | + val = 0 |
| 277 | + for i, _pos in enumerate(pos): |
| 278 | + res[i] = ((i_picked >> _pos) & 1) == 1 |
| 279 | + mask |= 1 << _pos |
| 280 | + val |= (res[i] & 1) << _pos |
| 281 | + |
| 282 | + nrm = 0.0 |
| 283 | + for i, _state in enumerate(self._state): |
| 284 | + if (mask & i) != val: |
| 285 | + self._state[i] = 0.0 |
| 286 | + else: |
| 287 | + nrm += np.abs(_state) ** 2 |
| 288 | + |
| 289 | + self._state *= 1.0 / np.sqrt(nrm) |
| 290 | + return res |
0 commit comments