microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/pipeline-issue-debugging

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/npm/qsharp/ux/atoms/layout.ts

938lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import { appendChildren, createSvgElements, setAttributes } from "./utils.js";
5
6// Zones will be rendered from top to bottom in array order
7// Supports both old format (rows count) and new format (rowStart/rowEnd)
8type ZoneDataInput = {
9 title: string;
10 kind: "register" | "interaction" | "measurement";
11} & (
12 | { rows: number; rowStart?: never; rowEnd?: never }
13 | { rowStart: number; rowEnd: number; skipRows?: number[]; rows?: never }
14);
15
16// Normalized internal format always uses rowStart/rowEnd
17type ZoneData = {
18 title: string;
19 rowStart: number;
20 rowEnd: number;
21 skipRows?: number[];
22 kind: "register" | "interaction" | "measurement";
23};
24
25export type ZoneLayout = {
26 cols: number;
27 zones: ZoneDataInput[];
28 // The instructions may assume certain columns are missing, but we don't render that visually.
29 // e.g. if `skipCols: [0,9]` was provided, then a `move(0,1)` would actually move to visual column 0,
30 // and a `move(1,10)` would move to visual column 8.
31 skipCols?: number[];
32 // If renumber is true, then all rows and columns will be renumbered in the visualization to be sequential starting from 0
33 renumber?: boolean;
34};
35
36const TWO_QUBIT_GATES = ["CX", "CY", "CZ", "RXX", "RYY", "RZZ", "SWAP"];
37
38// Normalize zone data to always use rowStart/rowEnd format
39function normalizeZones(zones: ZoneDataInput[]): ZoneData[] {
40 let nextRowStart = 0;
41 return zones.map((zone) => {
42 if ("rowStart" in zone && zone.rowStart !== undefined) {
43 // Verify if skipRows are within the rowStart and rowEnd range
44 if (zone.skipRows) {
45 if (
46 !zone.skipRows.every((r) => r >= zone.rowStart && r <= zone.rowEnd)
47 ) {
48 throw `Invalid skipRows in zone "${zone.title}": all skipRows must be between rowStart and rowEnd`;
49 }
50 }
51 // New format with explicit rowStart/rowEnd
52 return {
53 title: zone.title,
54 rowStart: zone.rowStart,
55 rowEnd: zone.rowEnd,
56 skipRows: zone.skipRows,
57 kind: zone.kind,
58 };
59 } else {
60 // Old format with rows count - convert to rowStart/rowEnd
61 const rowStart = nextRowStart;
62 const rowEnd = nextRowStart + zone.rows - 1;
63 nextRowStart = rowEnd + 1;
64 return {
65 title: zone.title,
66 rowStart,
67 rowEnd,
68 kind: zone.kind,
69 };
70 }
71 });
72}
73
74export type TraceData = {
75 metadata: any;
76 qubits: Array<[number, number]>;
77 steps: Array<{
78 id: string | number;
79 ops: Array<string>;
80 }>;
81};
82
83// const exampleLayout: ZoneLayout = {
84// "cols": 36,
85// "zones": [
86// { "title": "Register 1", "rows": 17, "kind": "register" },
87// { "title": "Interaction Zone", "rows": 4, "kind": "interaction" },
88// { "title": "Register 2", "rows": 17, "kind": "register" },
89// { "title": "Measurement Zone", "rows": 4, "kind": "measurement" },
90// ],
91// };
92
93type Location = [number, number, SVGElement?];
94
95const qubitSize = 10;
96const zoneSpacing = 8;
97const colPadding = 20;
98const initialScale = 1.5;
99const scaleStep = 0.15;
100const speedStep = 0.75;
101const zoneBoxCornerRadius = 3;
102const doublonCornerRadius = 5;
103
104// Used when no trace data is provided to fill the qubit mappings, assuming all register
105// zones are populated with sequentially numbered qubit ids from the top left to the bottom right.
106export function fillQubitLocations(
107 layout: ZoneLayout,
108): Array<[number, number]> {
109 const qubits: Array<[number, number]> = [];
110 const normalizedZones = normalizeZones(layout.zones);
111 normalizedZones.forEach((zone) => {
112 if (zone.kind === "register") {
113 for (let row = zone.rowStart; row <= zone.rowEnd; ++row) {
114 if (zone.skipRows && zone.skipRows.includes(row)) continue;
115 for (let col = 0; col < layout.cols; ++col) {
116 qubits.push([row, col]);
117 }
118 }
119 }
120 });
121
122 return qubits;
123}
124
125// Maps an instruction column to a visual column by accounting for skipped columns.
126// For each column in skipCols that is less than the instruction column, we subtract 1.
127function mapColumn(col: number, skipCols?: number[]): number {
128 if (!skipCols || skipCols.length === 0) {
129 return col;
130 }
131 const skippedBefore = skipCols.filter((c) => c < col).length;
132 return col - skippedBefore;
133}
134
135function parseMove(
136 op: string,
137 skipCols?: number[],
138): { qubit: number; to: Location } | undefined {
139 const match = op.match(/move?\((\d+), (\d+)\) (\d+)/);
140 if (match) {
141 const to: Location = [
142 parseInt(match[1]),
143 mapColumn(parseInt(match[2]), skipCols),
144 ];
145 return { qubit: parseInt(match[3]), to };
146 }
147 return undefined;
148}
149
150function parseGate(
151 op: string,
152): { gate: string; qubits: number[]; arg?: string } | undefined {
153 const match = op.match(/(\w+)\s*(\(.*\))? ([\d,\s]+)/);
154 if (match) {
155 const gate = match[1];
156 const qubits = match[3].split(",").map((s) => parseInt(s.trim()));
157 const arg = match[2]
158 ? match[2].substring(1, match[2].length - 2)
159 : undefined;
160 return { gate, qubits, arg };
161 }
162}
163
164function parseZoneOp(
165 op: string,
166 layout: ZoneLayout,
167): { op: "zone_cz" | "zone_mresetz"; zoneIndex: number } | undefined {
168 // If we get a naked 'cz' or 'mresetz', treat it as a zone op for the first zone of the matching type
169 if (op === "cz") {
170 return {
171 op: "zone_cz",
172 zoneIndex: layout.zones.findIndex((z) => z.kind === "interaction"),
173 };
174 }
175 if (op === "mresetz") {
176 return {
177 op: "zone_mresetz",
178 zoneIndex: layout.zones.findIndex((z) => z.kind === "measurement"),
179 };
180 }
181 const match = op.match(/^(zone_cz|zone_mresetz)\s+(\d+)$/);
182 if (match) {
183 return {
184 op: match[1] as "zone_cz" | "zone_mresetz",
185 zoneIndex: parseInt(match[2]),
186 };
187 }
188 return undefined;
189}
190
191/*
192We want to build up a cache of the qubit location at each 'n' steps so that scrubbing is fast.
193Storing for each step is too large for the number of steps we want to handle. Also, the building
194of the cache can take a long time, so we want to chunk it up to avoid blocking the UI thread.
195*/
196function TraceToGetLayoutFn(trace: TraceData, skipCols?: number[]) {
197 const STEP_SIZE = 100; // How many steps between each cache entry
198
199 const cacheEntries = Math.ceil(trace.steps.length / STEP_SIZE);
200 const entrySize = trace.qubits.length * 2; // row and col for each qubit
201 const cache = new Uint16Array(cacheEntries * entrySize);
202
203 // Fill initial locations
204 trace.qubits.forEach((loc, idx) => {
205 cache[idx * 2] = loc[0];
206 cache[idx * 2 + 1] = loc[1];
207 });
208
209 let lastIndexProcessed = 0;
210
211 // Update the layout (which should be the prior step layout)
212 function getNextStepLayout(stepIndex: number, layout: Uint16Array) {
213 if (stepIndex <= 0 || stepIndex >= trace.steps.length) {
214 throw "Step out of range";
215 }
216 // Extract the move operations in the prior step, to apply to the prior layout
217 const moves = trace.steps[stepIndex - 1].ops
218 .map((op) => parseMove(op, skipCols))
219 .filter((x) => x != undefined);
220
221 // Then apply them to the layout
222 moves.forEach((move) => {
223 layout[move.qubit * 2] = move.to[0];
224 layout[move.qubit * 2 + 1] = move.to[1];
225 });
226 }
227
228 // syncRunToIndex is used to force processing up to a certain index synchronously, such as
229 // when the user is scrubbing to a point not yet processed asynchronously.
230 function processChunk(syncRunToIndex = 0) {
231 if (lastIndexProcessed >= cacheEntries - 1) {
232 return; // Done
233 }
234
235 const startPriorIndex = lastIndexProcessed * entrySize;
236 const startIndex = (lastIndexProcessed + 1) * entrySize;
237
238 // Copy to the next entry as the starting point
239 cache.copyWithin(startIndex, startPriorIndex, startPriorIndex + entrySize);
240 const targetSlice = cache.subarray(startIndex, startIndex + entrySize);
241
242 // Run each step in the chunk and apply the moves to the cache
243 for (let stepOffset = 1; stepOffset <= STEP_SIZE; ++stepOffset) {
244 const stepIndex = lastIndexProcessed * STEP_SIZE + stepOffset;
245 if (stepIndex >= trace.steps.length) break;
246 getNextStepLayout(stepIndex, targetSlice);
247 }
248
249 // Queue up the next chunk
250 ++lastIndexProcessed;
251 if (syncRunToIndex > 0) {
252 // Process synchronously up to the requested index if not done yet
253 if (lastIndexProcessed < syncRunToIndex) processChunk(syncRunToIndex);
254 } else {
255 // Process the next chunk asynchronously
256 setTimeout(processChunk, 0);
257 }
258 }
259 // Kick off the async processing
260 processChunk();
261
262 // When a layout is requested for a step, cache the response. Most of the time the following
263 // step will be requested next, so we can easily just apply one set of moves to the prior layout.
264 let lastRequestedStep = -1;
265 const lastLayout = new Uint16Array(entrySize);
266
267 function getLayoutAtStep(step: number): Uint16Array {
268 if (step < 0 || step >= trace.steps.length) {
269 throw "Step out of range";
270 }
271
272 // If the cache hasn't processed up to the required step, do so now
273 if (step >= (lastIndexProcessed + 1) * STEP_SIZE) {
274 const entryIndex = Math.floor(step / STEP_SIZE);
275 processChunk(entryIndex);
276 }
277
278 // If the step exactly matches a cache entry, return that
279 if (step % STEP_SIZE === 0) {
280 const entryIndex = step / STEP_SIZE;
281 const startIndex = entryIndex * entrySize;
282 lastLayout.set(cache.subarray(startIndex, startIndex + entrySize));
283 lastRequestedStep = step;
284 return lastLayout;
285 }
286
287 // If the same as the last requested step, just return the cached layout
288 if (step === lastRequestedStep) {
289 return lastLayout;
290 }
291
292 // Otherwise, if the last requested step was the prior step, just apply the moves
293 if (step === lastRequestedStep + 1) {
294 getNextStepLayout(step, lastLayout);
295 ++lastRequestedStep;
296 return lastLayout;
297 }
298
299 // Otherwise, find the nearest prior cache entry and apply moves from there
300 const entryIndex = Math.floor(step / STEP_SIZE);
301 const startIndex = entryIndex * entrySize;
302 lastLayout.set(cache.subarray(startIndex, startIndex + entrySize));
303 for (
304 let stepIndex = entryIndex * STEP_SIZE + 1;
305 stepIndex <= step;
306 ++stepIndex
307 ) {
308 getNextStepLayout(stepIndex, lastLayout);
309 }
310 lastRequestedStep = step;
311 return lastLayout;
312 }
313
314 return getLayoutAtStep;
315}
316
317export class Layout {
318 container: SVGSVGElement;
319 width: number;
320 height: number;
321 scale: number = initialScale;
322 qubits: Location[];
323 rowOffsetMap: Map<number, number>; // Maps absolute row numbers to visual Y offsets
324 normalizedZones: ZoneData[];
325 effectiveCols: number; // Visual column count after accounting for skipCols
326 currentStep = 0;
327 trackParent: SVGGElement;
328 activeGates: SVGElement[] = [];
329 trace: TraceData;
330 getStepLayout: (step: number) => Uint16Array;
331 showTracks = true;
332 showDottedPath = true;
333 stepInterval = 500; // Used for playing and animations
334
335 constructor(
336 public layout: ZoneLayout,
337 trace: TraceData,
338 ) {
339 if (!trace.qubits?.length) {
340 trace.qubits = fillQubitLocations(layout);
341 } else if (layout.skipCols?.length) {
342 // Map the initial qubit column positions if skipCols is provided
343 trace.qubits = trace.qubits.map(([row, col]) => [
344 row,
345 mapColumn(col, layout.skipCols),
346 ]);
347 }
348 this.trace = trace;
349 this.getStepLayout = TraceToGetLayoutFn(trace, layout.skipCols);
350
351 this.qubits = structuredClone(trace.qubits);
352
353 this.container = document.createElementNS(
354 "http://www.w3.org/2000/svg",
355 "svg",
356 );
357
358 // Normalize zones to always use rowStart/rowEnd format
359 this.normalizedZones = normalizeZones(layout.zones);
360
361 // Calculate effective columns (visual count after skipping)
362 this.effectiveCols = layout.cols - (layout.skipCols?.length ?? 0);
363
364 const totalRows = this.normalizedZones.reduce(
365 (prev, curr) =>
366 prev + (curr.rowEnd - curr.rowStart + 1) - (curr.skipRows?.length ?? 0),
367 0,
368 );
369
370 this.height =
371 totalRows * qubitSize + zoneSpacing * (layout.zones.length + 1);
372 this.width = this.effectiveCols * qubitSize + colPadding;
373
374 setAttributes(this.container, {
375 viewBox: `-5 0 ${this.width} ${this.height}`,
376 width: `${this.width * this.scale}px`,
377 height: `${this.height * this.scale}px`,
378 });
379
380 // Loop through the zones, calculating the row offsets, and rendering the zones
381 // Maps absolute row numbers to visual Y offsets
382 this.rowOffsetMap = new Map();
383 let nextOffset = zoneSpacing;
384 let globalRowIndex = 0;
385 this.normalizedZones.forEach((zone, index) => {
386 globalRowIndex = this.renderZone(index, nextOffset, globalRowIndex);
387 for (let row = zone.rowStart; row <= zone.rowEnd; ++row) {
388 if (!zone.skipRows?.includes(row)) {
389 this.rowOffsetMap.set(row, nextOffset);
390 nextOffset += qubitSize;
391 }
392 }
393 nextOffset += zoneSpacing; // Add spacing after each zone
394 });
395
396 const colNumOffset = nextOffset - 8;
397 this.renderColNums(this.effectiveCols, colNumOffset);
398
399 // Put the track parent before the qubits, so the qubits render on top
400 this.trackParent = createSvgElements("g")[0] as SVGGElement;
401
402 this.trackParent.addEventListener("mouseover", (e) => {
403 const t = e.target as SVGElement;
404 if (t && t.dataset.qubitid) {
405 const qubitId = parseInt(t.dataset.qubitid);
406 this.highlightQubit(qubitId);
407 }
408 });
409 this.trackParent.addEventListener("mouseout", () => {
410 this.highlightQubit(null);
411 });
412
413 appendChildren(this.container, [this.trackParent]);
414
415 this.renderQubits();
416 }
417
418 private highlightQubit(qubit: number | null) {
419 if (qubit === null || qubit < 0 || qubit >= this.qubits.length) {
420 this.container
421 .querySelectorAll(".qs-atoms-qubit-highlight")
422 .forEach((elem) => {
423 elem.classList.remove("qs-atoms-qubit-highlight");
424 });
425 } else {
426 this.container
427 .querySelectorAll(`[data-qubitId="${qubit}"]`)
428 .forEach((elem) => {
429 elem.classList.add("qs-atoms-qubit-highlight");
430 });
431 }
432 }
433
434 private getOpsAtStep(step: number) {
435 if (step < 0 || step >= this.trace.steps.length) {
436 throw "Step out of range";
437 }
438 return this.trace.steps[step].ops;
439 }
440
441 private renderZone(
442 zoneIndex: number,
443 offset: number,
444 globalRowIndex: number,
445 ): number {
446 const zoneData = this.normalizedZones[zoneIndex];
447 const zoneRows =
448 zoneData.rowEnd -
449 zoneData.rowStart +
450 1 -
451 (zoneData.skipRows?.length ?? 0);
452 const g = createSvgElements("g")[0];
453 setAttributes(g, {
454 transform: `translate(0 ${offset})`,
455 class: "qs-atoms-zonebox",
456 });
457
458 if (zoneData.kind !== "interaction") {
459 // For non-interaction zones we draw one big rounded rectangle with lines between qubit rows & cols
460 const rect = createSvgElements("rect")[0];
461 setAttributes(rect, {
462 x: "0",
463 y: "0",
464 width: `${this.effectiveCols * qubitSize}`,
465 height: `${zoneRows * qubitSize}`,
466 rx: `${zoneBoxCornerRadius}`,
467 });
468 appendChildren(g, [rect]);
469
470 // Draw the lines between the rows
471 for (let i = 1; i < zoneRows; i++) {
472 const path = createSvgElements("path")[0];
473 setAttributes(path, {
474 d: `M 0,${i * qubitSize} h${this.effectiveCols * qubitSize}`,
475 });
476 appendChildren(g, [path]);
477 }
478 // Draw the lines between the columns
479 for (let i = 1; i < this.effectiveCols; i++) {
480 const path = createSvgElements("path")[0];
481 setAttributes(path, {
482 d: `M ${i * qubitSize},0 v${zoneRows * qubitSize}`,
483 });
484 appendChildren(g, [path]);
485 }
486 } else {
487 // For the interaction zone draw each doublon
488 for (let row = 0; row < zoneRows; ++row) {
489 for (let i = 0; i < this.effectiveCols; i += 2) {
490 const rect = createSvgElements("rect")[0];
491 setAttributes(rect, {
492 x: `${i * qubitSize}`,
493 y: `${row * qubitSize}`,
494 width: `${qubitSize * 2}`,
495 height: `${qubitSize}`,
496 rx: `${doublonCornerRadius}`,
497 });
498 const path = createSvgElements("path")[0];
499 setAttributes(path, {
500 d: `M ${(i + 1) * qubitSize},${row * qubitSize} v${qubitSize}`,
501 });
502 appendChildren(g, [rect, path]);
503 }
504 }
505 }
506
507 // Number the rows using the absolute row numbers from rowStart to rowEnd
508 let i = 0;
509 for (let rowNum = zoneData.rowStart; rowNum <= zoneData.rowEnd; ++rowNum) {
510 if (zoneData.skipRows && zoneData.skipRows.includes(rowNum)) continue;
511 const label = createSvgElements("text")[0];
512 setAttributes(label, {
513 x: `${this.effectiveCols * qubitSize + 5}`,
514 y: `${i * qubitSize + 5}`,
515 class: "qs-atoms-label",
516 });
517 label.textContent = `${this.layout.renumber ? globalRowIndex : rowNum}`;
518 appendChildren(g, [label]);
519 ++i;
520 ++globalRowIndex;
521 }
522
523 // Draw the title
524 const text = createSvgElements("text")[0];
525 setAttributes(text, {
526 x: "1",
527 y: "-1",
528 class: "qs-atoms-zone-text",
529 });
530 text.textContent = zoneData.title;
531
532 appendChildren(g, [text]);
533 appendChildren(this.container, [g]);
534 return globalRowIndex;
535 }
536
537 private renderQubits() {
538 const elems = this.qubits.map((location, index) => {
539 const [x, y] = this.getQubitCenter(index);
540
541 // Safari has an issue animating multiple attributes concurrently, which we need
542 // to do to move the qubit (animate 'cx' and 'cy'), so instead set cx and cy to 0
543 // and position the qubit with a transform (see https://stackoverflow.com/a/72022385/1674945)
544
545 const circle = createSvgElements("circle")[0];
546 setAttributes(circle, {
547 cx: `0`,
548 cy: `0`,
549 r: `2`,
550 class: "qs-atoms-qubit",
551 "data-qubitid": `${index}`,
552 });
553 circle.addEventListener("mouseover", () => this.highlightQubit(index));
554 circle.addEventListener("mouseout", () => this.highlightQubit(null));
555 // Animation sets the transform as a style attribute, not an element attribute.
556 // Also note, when animating the CSS it requires the 'px' length type (unlike the attribute).
557 circle.style.transform = `translate(${x}px, ${y}px)`;
558 location[2] = circle;
559 return circle;
560 });
561
562 appendChildren(this.container, elems);
563 }
564
565 private renderColNums(cols: number, offset: number) {
566 const g = createSvgElements("g")[0];
567 setAttributes(g, {
568 transform: `translate(0 ${offset})`,
569 });
570 // Number the columns, showing original column numbers (skipping those in skipCols)
571 const skipCols = this.layout.skipCols ?? [];
572 let visualCol = 0;
573 for (let origCol = 0; visualCol < cols; ++origCol) {
574 if (skipCols.includes(origCol)) continue;
575 const label = createSvgElements("text")[0];
576 setAttributes(label, {
577 x: `${visualCol * qubitSize + 5}`,
578 y: `5`,
579 class: "qs-atoms-label",
580 });
581 label.textContent = `${this.layout.renumber ? visualCol : origCol}`;
582 appendChildren(g, [label]);
583 ++visualCol;
584 }
585 appendChildren(this.container, [g]);
586 }
587
588 renderGateOnQubit(qubit: number, gate: string, arg?: string) {
589 if (gate == "RESET") gate = "R";
590 const [x, y] = this.getQubitCenter(qubit);
591
592 const gateClass =
593 gate === "MZ"
594 ? "qs-atoms-gate qs-atoms-gate-mz"
595 : gate === "R"
596 ? "qs-atoms-gate qs-atoms-gate-reset"
597 : "qs-atoms-gate";
598
599 const g = createSvgElements("g")[0];
600 setAttributes(g, {
601 transform: `translate(${x - qubitSize / 2} ${y - qubitSize / 2})`,
602 class: "qs-atoms-gate",
603 });
604
605 if (gate === "CZ") {
606 // Render the rounded doublon box in a bright color and x--x inside
607 const [rect, path, leftDot, rightDot] = createSvgElements(
608 "rect",
609 "path",
610 "circle",
611 "circle",
612 );
613 setAttributes(rect, {
614 x: `0`,
615 y: "0",
616 width: `${qubitSize * 2}`,
617 height: `${qubitSize}`,
618 rx: `${doublonCornerRadius}`,
619 class: "qs-atoms-zonebox qs-atoms-gate-cz",
620 });
621 // <path d= "M45,5 h10" stroke-width="1.5" stroke="black"/>
622 // <circle cx="45" cy="5" r="2" stroke-width="0" fill="#123" />
623 // <circle cx="55" cy="5" r="2" stroke-width="0" fill="#123" />
624 setAttributes(path, {
625 fill: "none",
626 stroke: "black",
627 "stroke-width": "1.5",
628 d: "M5,5 h10",
629 });
630 setAttributes(leftDot, {
631 cx: "5",
632 cy: "5",
633 r: "2",
634 "stroke-width": "0",
635 fill: "#123",
636 });
637 setAttributes(rightDot, {
638 cx: "15",
639 cy: "5",
640 r: "2",
641 "stroke-width": "0",
642 fill: "#123",
643 });
644 appendChildren(g, [rect, path, leftDot, rightDot]);
645 } else {
646 const [rect, text] = createSvgElements("rect", "text");
647 setAttributes(rect, {
648 x: "0.5",
649 y: "0.5",
650 width: `${qubitSize - 1}`,
651 height: `${qubitSize - 1}`,
652 class: gateClass,
653 });
654 setAttributes(text, {
655 x: "5",
656 y: arg ? "2.75" : "5",
657 class: "qs-atoms-gate-text",
658 });
659 text.textContent = gate;
660
661 appendChildren(g, [rect, text]);
662
663 if (arg) {
664 const argText = createSvgElements("text")[0];
665 setAttributes(argText, {
666 x: "5",
667 y: "7",
668 class: "qs-atoms-gate-text qs-atoms-gate-text-small",
669 textLength: "8",
670 });
671 text.classList.add("qs-atoms-gate-text-small");
672 argText.textContent = arg;
673 appendChildren(g, [argText]);
674 }
675 }
676
677 appendChildren(this.container, [g]);
678 this.activeGates.push(g);
679 }
680
681 clearGates() {
682 this.activeGates.forEach((gate) => {
683 gate.parentElement?.removeChild(gate);
684 });
685 // TODO: Clear doublons too
686 this.activeGates = [];
687 }
688
689 zoomIn() {
690 this.scale += scaleStep * this.scale;
691 setAttributes(this.container, {
692 width: `${this.width * this.scale}px`,
693 height: `${this.height * this.scale}px`,
694 });
695 }
696
697 zoomOut() {
698 this.scale -= scaleStep * this.scale;
699 setAttributes(this.container, {
700 width: `${this.width * this.scale}px`,
701 height: `${this.height * this.scale}px`,
702 });
703 }
704
705 faster() {
706 this.stepInterval = this.stepInterval * speedStep;
707 }
708
709 slower() {
710 this.stepInterval = this.stepInterval / speedStep;
711 }
712
713 cycleAnimation() {
714 if (!this.showTracks) {
715 this.showTracks = true;
716 this.showDottedPath = true;
717 } else if (this.showTracks && this.showDottedPath) {
718 this.showDottedPath = false;
719 } else {
720 this.showTracks = false;
721 this.showDottedPath = false;
722 }
723 this.gotoStep(this.currentStep);
724 }
725
726 getQubitRowOffset(row: number) {
727 const offset = this.rowOffsetMap.get(row);
728 if (offset === undefined) {
729 throw `Row ${row} not found in layout`;
730 }
731 return offset;
732 }
733
734 // Returns array of qubit indices that are currently in the specified zone
735 getQubitsInZone(zoneIndex: number): number[] {
736 const zone = this.normalizedZones[zoneIndex];
737 if (!zone) {
738 throw `Zone ${zoneIndex} not found`;
739 }
740 const qubitsInZone: number[] = [];
741 for (let i = 0; i < this.qubits.length; i++) {
742 const [row] = this.qubits[i];
743 if (row >= zone.rowStart && row <= zone.rowEnd) {
744 qubitsInZone.push(i);
745 }
746 }
747 return qubitsInZone;
748 }
749
750 getLocationCenter(row: number, col: number): [number, number] {
751 const x = col * qubitSize + qubitSize / 2;
752 const y = this.getQubitRowOffset(row) + qubitSize / 2;
753 return [x, y];
754 }
755
756 getQubitCenter(qubit: number): [number, number] {
757 if (this.qubits[qubit] == undefined) {
758 throw "Qubit not found";
759 }
760
761 const [row, col] = this.qubits[qubit];
762 return this.getLocationCenter(row, col);
763 }
764
765 gotoStep(step: number) {
766 // Remove prior rendering's gates, trails, or remaining animations
767 this.clearGates();
768 this.trackParent.replaceChildren();
769 this.container.getAnimations({ subtree: true }).forEach((anim) => {
770 anim.cancel();
771 });
772
773 const forwards = step > this.currentStep;
774 this.currentStep = step;
775
776 // When on step 0, just layout the qubits per index 0
777 // When on step 1, layout per index 0 then apply the gates/moves per index 0
778 // When on step 2, layout per index 1 then apply the gates/moves per index 1
779 // etc. until when on step n + 1, layout per index n and apply per index n
780 const qubitLocationIndex = step === 0 ? 0 : step - 1;
781
782 // Update all qubit locations
783 const qubitLayout = this.getStepLayout(qubitLocationIndex);
784 const qubitCount = qubitLayout.length / 2;
785 for (let idx = 0; idx < qubitCount; ++idx) {
786 const elem = this.qubits[idx][2];
787 if (elem === undefined) {
788 throw "Invalid qubit index in step";
789 }
790 const loc: Location = [qubitLayout[idx * 2], qubitLayout[idx * 2 + 1]];
791 this.qubits[idx] = [loc[0], loc[1], elem]; // Update the location
792
793 // Get the offset for the location and move it there
794 const [x, y] = this.getQubitCenter(idx);
795 elem.style.transform = `translate(${x}px, ${y}px)`;
796 }
797
798 // Now apply the ops
799 if (step > 0) {
800 const duration = forwards ? this.stepInterval / 2 : 0;
801 const ops = this.getOpsAtStep(qubitLocationIndex);
802 let trailId = 0;
803 ops.forEach((op) => {
804 const move = parseMove(op, this.layout.skipCols);
805 if (move) {
806 // Apply the move animation
807 const [oldX, oldY] = this.getQubitCenter(move.qubit);
808 const [newX, newY] = this.getLocationCenter(move.to[0], move.to[1]);
809 const qubit = this.qubits[move.qubit][2];
810 if (!qubit) throw "Invalid qubit index";
811 if (this.showTracks) {
812 if (this.showDottedPath) {
813 // Render a hollow circle at the start position
814 const [startCircle, trail] = createSvgElements("circle", "line");
815 setAttributes(startCircle, {
816 cx: `${oldX}`,
817 cy: `${oldY}`,
818 class: `qs-atoms-qubit-trail-start`,
819 "data-qubitid": `${move.qubit}`,
820 });
821
822 const id = `dotted-trail-${trailId++}`;
823 setAttributes(trail, {
824 id,
825 x1: `${oldX}`,
826 y1: `${oldY}`,
827 x2: `${newX}`,
828 y2: `${newY}`,
829 class: "qs-atoms-qubit-trail",
830 "data-qubitid": `${move.qubit}`,
831 });
832 appendChildren(this.trackParent, [trail, startCircle]);
833 } else {
834 const id = `gradient-${trailId++}`;
835 // Draw the path of any qubit movements
836 const [gradient, trail] = createSvgElements(
837 "linearGradient",
838 "line",
839 );
840 setAttributes(gradient, {
841 id,
842 gradientUnits: "userSpaceOnUse",
843 x1: `${oldX}`,
844 y1: `${oldY}`,
845 x2: `${newX}`,
846 y2: `${newY}`,
847 });
848 gradient.innerHTML = `<stop offset="0%" stop-color="gray" stop-opacity="0.2"/><stop offset="100%" stop-color="gray" stop-opacity="0.8"/>`;
849 setAttributes(trail, {
850 x1: `${oldX}`,
851 y1: `${oldY}`,
852 x2: `${newX}`,
853 y2: `${newY}`,
854 style: `stroke-width: 2px; stroke: url(#${id})`,
855 });
856 appendChildren(this.trackParent, [gradient, trail]);
857 }
858 }
859 qubit
860 .animate(
861 [
862 { transform: `translate(${oldX}px, ${oldY}px)` },
863 { transform: `translate(${newX}px, ${newY}px)` },
864 ],
865 {
866 duration: this.showDottedPath ? 50 : duration,
867 fill: "forwards",
868 easing: "ease",
869 },
870 )
871 .finished.then((anim) => {
872 anim.commitStyles();
873 anim.cancel();
874 });
875 // TODO: Check if you can/should cancel when scrubbing
876 } else {
877 // Check if it's a zone operation
878 const zoneOp = parseZoneOp(op, this.layout);
879 if (zoneOp) {
880 const qubitsInZone = this.getQubitsInZone(zoneOp.zoneIndex);
881 if (zoneOp.op === "zone_cz") {
882 // For zone_cz, render CZ gates on pairs of qubits in doublons
883 // Group qubits by their row, then pair adjacent columns
884 const qubitsByRow = new Map<number, number[]>();
885 for (const qubitIdx of qubitsInZone) {
886 const [row] = this.qubits[qubitIdx];
887 if (!qubitsByRow.has(row)) {
888 qubitsByRow.set(row, []);
889 }
890 qubitsByRow.get(row)!.push(qubitIdx);
891 }
892 // For each row, find pairs in doublons (col 0-1, 2-3, 4-5, etc.)
893 for (const [, qubitsInRow] of qubitsByRow) {
894 // Sort by column
895 qubitsInRow.sort(
896 (a, b) => this.qubits[a][1] - this.qubits[b][1],
897 );
898 for (let i = 0; i < qubitsInRow.length - 1; i++) {
899 const q1 = qubitsInRow[i];
900 const q2 = qubitsInRow[i + 1];
901 const col1 = this.qubits[q1][1];
902 const col2 = this.qubits[q2][1];
903 // Check if they're in the same doublon (even-odd pair)
904 if (Math.floor(col1 / 2) === Math.floor(col2 / 2)) {
905 this.renderGateOnQubit(q1, "CZ");
906 i++; // Skip the next qubit since we've paired it
907 }
908 }
909 }
910 } else if (zoneOp.op === "zone_mresetz") {
911 // For zone_mresetz, render MZ gate on all qubits in the zone
912 for (const qubitIdx of qubitsInZone) {
913 this.renderGateOnQubit(qubitIdx, "MZ");
914 }
915 }
916 } else {
917 // Wasn't a move or zone op, so render the gate
918 const gate = parseGate(op);
919 if (!gate) throw `Invalid gate: ${op}`;
920 const arg = gate.arg ? gate.arg.substring(0, 4) : undefined;
921 const isTwoQubitGate = TWO_QUBIT_GATES.includes(
922 gate.gate.toUpperCase(),
923 );
924 // Handle the case where an op is a list of qubits to apply the same op to
925 for (let i = 0; i < gate.qubits.length; i++) {
926 if (isTwoQubitGate && i % 2 !== 0) continue;
927 this.renderGateOnQubit(
928 gate.qubits[i],
929 gate.gate.toUpperCase(),
930 arg,
931 );
932 }
933 }
934 }
935 });
936 }
937 }
938}
939