microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
source/npm/qsharp/src/data-structures/legacyCircuitUpdate.ts
441lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT license. |
| 3 | |
| 4 | import { |
| 5 | Circuit, |
| 6 | CircuitGroup, |
| 7 | ComponentGrid, |
| 8 | CURRENT_VERSION, |
| 9 | isCircuit, |
| 10 | isCircuitGroup, |
| 11 | isOperation, |
| 12 | Operation, |
| 13 | Qubit, |
| 14 | } from "./circuit.js"; |
| 15 | import { Register } from "./register.js"; |
| 16 | |
| 17 | export type ToCircuitGroupResult = |
| 18 | | { ok: true; circuitGroup: CircuitGroup } |
| 19 | | { ok: false; error: string }; |
| 20 | |
| 21 | /** |
| 22 | * Ensures that the given circuit object is a CircuitGroup, doing any |
| 23 | * necessary conversions from Circuit or legacy formats. |
| 24 | * |
| 25 | * @param circuit The circuit to convert. |
| 26 | * @returns The result of the conversion. |
| 27 | */ |
| 28 | export function toCircuitGroup(circuit: any): ToCircuitGroupResult { |
| 29 | const emptyCircuit: Circuit = { |
| 30 | qubits: [], |
| 31 | componentGrid: [], |
| 32 | }; |
| 33 | |
| 34 | const emptyCircuitGroup: CircuitGroup = { |
| 35 | version: CURRENT_VERSION, |
| 36 | circuits: [emptyCircuit], |
| 37 | }; |
| 38 | |
| 39 | if (circuit && Object.keys(circuit).length === 0) { |
| 40 | return { ok: true, circuitGroup: emptyCircuitGroup }; |
| 41 | } |
| 42 | |
| 43 | if (circuit?.version) { |
| 44 | const version = circuit.version; |
| 45 | if (isCircuitGroup(circuit)) { |
| 46 | return { ok: true, circuitGroup: circuit }; |
| 47 | } else if (isCircuit(circuit)) { |
| 48 | return { ok: true, circuitGroup: { version, circuits: [circuit] } }; |
| 49 | } else { |
| 50 | return { |
| 51 | ok: false, |
| 52 | error: "Unknown schema: file is neither a CircuitGroup nor a Circuit.", |
| 53 | }; |
| 54 | } |
| 55 | } else if (isCircuit(circuit)) { |
| 56 | return { |
| 57 | ok: true, |
| 58 | circuitGroup: { version: CURRENT_VERSION, circuits: [circuit] }, |
| 59 | }; |
| 60 | } else if ( |
| 61 | circuit?.operations && |
| 62 | Array.isArray(circuit.operations) && |
| 63 | circuit?.qubits && |
| 64 | Array.isArray(circuit.qubits) |
| 65 | ) { |
| 66 | // If it has "operations" and "qubits", it is a legacy schema |
| 67 | return tryConvertLegacySchema(circuit); |
| 68 | } else { |
| 69 | return { |
| 70 | ok: false, |
| 71 | error: "Unknown schema: file does not match any known format.", |
| 72 | }; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Attempts to convert a legacy circuit schema to a CircuitGroup. |
| 78 | * |
| 79 | * @param circuit The legacy circuit object to convert. |
| 80 | * @returns A ToCircuitGroupResult containing the converted CircuitGroup on success, |
| 81 | * or an error message on failure. |
| 82 | */ |
| 83 | function tryConvertLegacySchema(circuit: any): ToCircuitGroupResult { |
| 84 | try { |
| 85 | const qubits: Qubit[] = circuit.qubits.map((qubit: any, idx: number) => { |
| 86 | if ( |
| 87 | typeof qubit !== "object" || |
| 88 | qubit === null || |
| 89 | typeof qubit.id !== "number" |
| 90 | ) { |
| 91 | throw new Error(`Invalid qubit at index ${idx}.`); |
| 92 | } |
| 93 | return { |
| 94 | id: qubit.id, |
| 95 | numResults: qubit.numChildren || 0, |
| 96 | }; |
| 97 | }); |
| 98 | |
| 99 | const operationList = circuit.operations.map((op: any, idx: number) => { |
| 100 | try { |
| 101 | return toOperation(op); |
| 102 | } catch (e) { |
| 103 | throw new Error( |
| 104 | `Failed to convert operation at index ${idx}: ${(e as Error).message}`, |
| 105 | ); |
| 106 | } |
| 107 | }); |
| 108 | |
| 109 | if (!operationList.every(isOperation)) { |
| 110 | return { |
| 111 | ok: false, |
| 112 | error: "Unknown schema: file contains invalid operations.", |
| 113 | }; |
| 114 | } |
| 115 | |
| 116 | const componentGrid = operationListToGrid(operationList, qubits.length); |
| 117 | |
| 118 | return { |
| 119 | ok: true, |
| 120 | circuitGroup: { |
| 121 | version: CURRENT_VERSION, |
| 122 | circuits: [ |
| 123 | { |
| 124 | qubits, |
| 125 | componentGrid, |
| 126 | }, |
| 127 | ], |
| 128 | }, |
| 129 | }; |
| 130 | } catch (e) { |
| 131 | return { |
| 132 | ok: false, |
| 133 | error: `Legacy schema: ${e instanceof Error ? e.message : String(e)}`, |
| 134 | }; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Converts a legacy operation object to the new Operation format. |
| 140 | * |
| 141 | * @param op The operation to convert. |
| 142 | * @returns The converted Operation. |
| 143 | */ |
| 144 | function toOperation(op: any): Operation { |
| 145 | let targets = []; |
| 146 | if (op.targets) { |
| 147 | targets = op.targets.map((t: any) => { |
| 148 | return { |
| 149 | qubit: t.qId, |
| 150 | result: t.cId, |
| 151 | }; |
| 152 | }); |
| 153 | } |
| 154 | let controls = undefined; |
| 155 | if (op.controls) { |
| 156 | controls = op.controls.map((c: any) => { |
| 157 | return { |
| 158 | qubit: c.qId, |
| 159 | result: c.cId, |
| 160 | }; |
| 161 | }); |
| 162 | } |
| 163 | |
| 164 | if (op.isMeasurement) { |
| 165 | return { |
| 166 | ...op, |
| 167 | kind: "measurement", |
| 168 | qubits: controls || [], |
| 169 | results: targets, |
| 170 | } as Operation; |
| 171 | } else { |
| 172 | const ket = op.gate === undefined ? "" : getKetLabel(op.gate); |
| 173 | if (ket.length > 0) { |
| 174 | return { |
| 175 | ...op, |
| 176 | kind: "ket", |
| 177 | gate: ket, |
| 178 | targets, |
| 179 | }; |
| 180 | } else { |
| 181 | const convertedOp: Operation = { |
| 182 | ...op, |
| 183 | kind: "unitary", |
| 184 | targets, |
| 185 | controls, |
| 186 | }; |
| 187 | if (op.displayArgs) { |
| 188 | convertedOp.args = [op.displayArgs]; |
| 189 | // Assume the parameter is always "theta" for now |
| 190 | convertedOp.params = [{ name: "theta", type: "Double" }]; |
| 191 | } |
| 192 | if (op.children) { |
| 193 | convertedOp.children = [ |
| 194 | { |
| 195 | components: op.children.map((child: any) => toOperation(child)), |
| 196 | }, |
| 197 | ]; |
| 198 | } |
| 199 | return convertedOp; |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Get the label from a ket string. |
| 206 | * |
| 207 | * @param ket The ket string to extract the label from. |
| 208 | * @returns The label extracted from the ket string. |
| 209 | */ |
| 210 | function getKetLabel(ket: string): string { |
| 211 | // Check that the ket conforms to the format |{label}> or |{label}⟩ |
| 212 | // Be overly permissive with the ket format, allowing for various closing characters |
| 213 | const ketRegex = /^\|([^\s〉⟩〉>]+)(?:[〉⟩〉>])$/; |
| 214 | |
| 215 | // Match the ket string against the regex |
| 216 | const match = ket.match(ketRegex); |
| 217 | |
| 218 | // If valid, return the inner label (captured group 1), otherwise return an empty string |
| 219 | return match ? match[1] : ""; |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | * Converts a list of operations into a 2D grid of operations in col-row format. |
| 224 | * Operations will be left-justified as much as possible in the resulting grid. |
| 225 | * Children operations are recursively converted into a grid. |
| 226 | * |
| 227 | * @param operations Array of operations. |
| 228 | * @param numQubits Number of qubits in the circuit. |
| 229 | * |
| 230 | * @returns A 2D array of operations. |
| 231 | */ |
| 232 | function operationListToGrid( |
| 233 | operations: Operation[], |
| 234 | numQubits: number, |
| 235 | ): ComponentGrid { |
| 236 | operations.forEach((op) => { |
| 237 | // The children data structure is a grid, so checking if it is |
| 238 | // length 1 is actually checking if it has a single column, |
| 239 | // or in other words, we are checking if its children are in a single list. |
| 240 | // If the operation has children in a single list, it needs to be converted to a grid. |
| 241 | // If it was already converted to a grid, but the grid was still a single list, |
| 242 | // then doing it again won't effect anything. |
| 243 | if (op.children && op.children.length == 1) { |
| 244 | op.children = operationListToGrid(op.children[0].components, numQubits); |
| 245 | } |
| 246 | }); |
| 247 | |
| 248 | return removePadding(operationListToPaddedArray(operations, numQubits)).map( |
| 249 | (col) => ({ |
| 250 | components: col, |
| 251 | }), |
| 252 | ); |
| 253 | } |
| 254 | |
| 255 | /** |
| 256 | * Converts a list of operations into a padded 2D array of operations. |
| 257 | * |
| 258 | * @param operations Array of operations. |
| 259 | * @param numQubits Number of qubits in the circuit. |
| 260 | * |
| 261 | * @returns A 2D array of operations padded with `null`s. |
| 262 | */ |
| 263 | function operationListToPaddedArray( |
| 264 | operations: Operation[], |
| 265 | numQubits: number, |
| 266 | ): (Operation | null)[][] { |
| 267 | if (operations.length === 0) return []; |
| 268 | |
| 269 | // Group operations based on registers |
| 270 | const groupedOps: number[][] = groupOperations(operations, numQubits); |
| 271 | |
| 272 | // Align operations on multiple registers |
| 273 | const alignedOps: (number | null)[][] = transformToColRow( |
| 274 | alignOps(groupedOps), |
| 275 | ); |
| 276 | |
| 277 | const operationArray: (Operation | null)[][] = alignedOps.map((col) => |
| 278 | col.map((opIdx) => { |
| 279 | if (opIdx == null) return null; |
| 280 | return operations[opIdx]; |
| 281 | }), |
| 282 | ); |
| 283 | |
| 284 | return operationArray; |
| 285 | } |
| 286 | |
| 287 | /** |
| 288 | * Removes padding (`null` values) from a 2D array of operations. |
| 289 | * |
| 290 | * @param operations 2D array of operations padded with `null`s. |
| 291 | * |
| 292 | * @returns A 2D array of operations without `null` values. |
| 293 | */ |
| 294 | function removePadding(operations: (Operation | null)[][]): Operation[][] { |
| 295 | return operations.map((col) => col.filter((op) => op != null)); |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Transforms a row-col 2D array into an equivalent col-row 2D array. |
| 300 | * |
| 301 | * @param alignedOps 2D array of operations in row-col format. |
| 302 | * |
| 303 | * @returns 2D array of operations in col-row format. |
| 304 | */ |
| 305 | function transformToColRow( |
| 306 | alignedOps: (number | null)[][], |
| 307 | ): (number | null)[][] { |
| 308 | if (alignedOps.length === 0) return []; |
| 309 | |
| 310 | const numRows = alignedOps.length; |
| 311 | const numCols = Math.max(...alignedOps.map((row) => row.length)); |
| 312 | |
| 313 | const colRowArray: (number | null)[][] = Array.from({ length: numCols }, () => |
| 314 | Array(numRows).fill(null), |
| 315 | ); |
| 316 | |
| 317 | for (let row = 0; row < numRows; row++) { |
| 318 | for (let col = 0; col < alignedOps[row].length; col++) { |
| 319 | colRowArray[col][row] = alignedOps[row][col]; |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | return colRowArray; |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Group gates provided by operations into their respective registers. |
| 328 | * |
| 329 | * @param operations Array of operations. |
| 330 | * @param numQubits Number of qubits in the circuit. |
| 331 | * |
| 332 | * @returns 2D array of indices where `groupedOps[i][j]` is the index of the operations |
| 333 | * at register `i` and column `j` (not yet aligned/padded). |
| 334 | */ |
| 335 | function groupOperations( |
| 336 | operations: Operation[], |
| 337 | numQubits: number, |
| 338 | ): number[][] { |
| 339 | const groupedOps: number[][] = Array.from( |
| 340 | Array(numQubits), |
| 341 | () => new Array(0), |
| 342 | ); |
| 343 | operations.forEach((operation, instrIdx) => { |
| 344 | const [minRegIdx, maxRegIdx] = getMinMaxRegIdx(operation, numQubits); |
| 345 | if (minRegIdx > -1 && maxRegIdx > -1) { |
| 346 | // Add operation also to registers that are in-between target registers |
| 347 | // so that other gates won't render in the middle. |
| 348 | for (let i = minRegIdx; i <= maxRegIdx; i++) { |
| 349 | groupedOps[i].push(instrIdx); |
| 350 | } |
| 351 | } |
| 352 | }); |
| 353 | return groupedOps; |
| 354 | } |
| 355 | |
| 356 | /** |
| 357 | * Aligns operations by padding registers with `null`s to make sure that multiqubit |
| 358 | * gates are in the same column. |
| 359 | * e.g. ---[x]---[x]-- |
| 360 | * ----------|--- |
| 361 | * |
| 362 | * @param ops 2D array of operations. Each row represents a register |
| 363 | * and the operations acting on it (in-order). |
| 364 | * |
| 365 | * @returns 2D array of aligned operations padded with `null`s. |
| 366 | */ |
| 367 | function alignOps(ops: number[][]): (number | null)[][] { |
| 368 | let maxNumOps: number = Math.max(0, ...ops.map((regOps) => regOps.length)); |
| 369 | let col = 0; |
| 370 | // Deep copy ops to be returned as paddedOps |
| 371 | const paddedOps: (number | null)[][] = ops.map((regOps) => [...regOps]); |
| 372 | while (col < maxNumOps) { |
| 373 | for (let regIdx = 0; regIdx < paddedOps.length; regIdx++) { |
| 374 | const reg: (number | null)[] = paddedOps[regIdx]; |
| 375 | if (reg.length <= col) continue; |
| 376 | |
| 377 | // Should never be null (nulls are only padded to previous columns) |
| 378 | const opIdx: number | null = reg[col]; |
| 379 | |
| 380 | // Get position of gate |
| 381 | const targetsPos: number[] = paddedOps.map((regOps) => |
| 382 | regOps.indexOf(opIdx), |
| 383 | ); |
| 384 | const gatePos: number = Math.max(-1, ...targetsPos); |
| 385 | |
| 386 | // If current column is not desired gate position, pad with null |
| 387 | if (col < gatePos) { |
| 388 | paddedOps[regIdx].splice(col, 0, null); |
| 389 | maxNumOps = Math.max(maxNumOps, paddedOps[regIdx].length); |
| 390 | } |
| 391 | } |
| 392 | col++; |
| 393 | } |
| 394 | return paddedOps; |
| 395 | } |
| 396 | |
| 397 | /** |
| 398 | * Get the minimum and maximum register indices for a given operation. |
| 399 | * |
| 400 | * @param operation The operation for which to get the register indices. |
| 401 | * @param numQubits The number of qubits in the circuit. |
| 402 | * @returns A tuple containing the minimum and maximum register indices. |
| 403 | */ |
| 404 | function getMinMaxRegIdx( |
| 405 | operation: Operation, |
| 406 | numQubits: number, |
| 407 | ): [number, number] { |
| 408 | let targets: Register[]; |
| 409 | let controls: Register[]; |
| 410 | switch (operation.kind) { |
| 411 | case "measurement": |
| 412 | targets = operation.results; |
| 413 | controls = operation.qubits; |
| 414 | break; |
| 415 | case "unitary": |
| 416 | targets = operation.targets; |
| 417 | controls = operation.controls || []; |
| 418 | break; |
| 419 | case "ket": |
| 420 | targets = operation.targets; |
| 421 | controls = []; |
| 422 | break; |
| 423 | } |
| 424 | |
| 425 | const qRegs = [...controls, ...targets] |
| 426 | .filter(({ result }) => result === undefined) |
| 427 | .map(({ qubit }) => qubit); |
| 428 | const clsControls: Register[] = controls.filter( |
| 429 | ({ result }) => result !== undefined, |
| 430 | ); |
| 431 | const isClassicallyControlled: boolean = clsControls.length > 0; |
| 432 | if (!isClassicallyControlled && qRegs.length === 0) return [-1, -1]; |
| 433 | // If operation is classically-controlled, pad all qubit registers. Otherwise, only pad |
| 434 | // the contiguous range of registers that it covers. |
| 435 | const minRegIdx: number = isClassicallyControlled ? 0 : Math.min(...qRegs); |
| 436 | const maxRegIdx: number = isClassicallyControlled |
| 437 | ? numQubits - 1 |
| 438 | : Math.max(...qRegs); |
| 439 | |
| 440 | return [minRegIdx, maxRegIdx]; |
| 441 | } |
| 442 | |