microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
minestarks/circuit-magic

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/src/data-structures/legacyCircuitUpdate.ts

441lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4import {
5 Circuit,
6 CircuitGroup,
7 ComponentGrid,
8 CURRENT_VERSION,
9 isCircuit,
10 isCircuitGroup,
11 isOperation,
12 Operation,
13 Qubit,
14} from "./circuit.js";
15import { Register } from "./register.js";
16
17export 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 */
28export 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 */
83function 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 */
144function 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 */
210function 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 */
232function 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 */
263function 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 */
294function 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 */
305function 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 */
335function 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 */
367function 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 */
404function 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