From 4192f29c60ed83359602a027d97a9a5ff357ebda Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:21:03 -0500 Subject: [PATCH] Copy over quantum-viz.js source code (#1973) This eliminates the dependency on the `@microsoft/quantum-viz.js` package and moves the source over into the `qsharp-lang` package. --- compiler/qsc_circuit/src/circuit.rs | 2 +- npm/qsharp/src/browser.ts | 2 + npm/qsharp/src/compiler/compiler.ts | 2 +- npm/qsharp/src/debug-service/debug-service.ts | 2 +- npm/qsharp/src/shared/README.md | 1 + npm/qsharp/src/shared/circuit.ts | 72 +++ npm/qsharp/src/shared/register.ts | 41 ++ npm/qsharp/ux/circuit-vis/README.md | 1 + npm/qsharp/ux/circuit-vis/circuit.ts | 10 + npm/qsharp/ux/circuit-vis/constants.ts | 39 ++ .../ux/circuit-vis/formatters/formatUtils.ts | 220 +++++++ .../circuit-vis/formatters/gateFormatter.ts | 557 ++++++++++++++++++ .../circuit-vis/formatters/inputFormatter.ts | 78 +++ .../formatters/registerFormatter.ts | 118 ++++ npm/qsharp/ux/circuit-vis/index.ts | 30 + npm/qsharp/ux/circuit-vis/metadata.ts | 56 ++ npm/qsharp/ux/circuit-vis/process.ts | 536 +++++++++++++++++ npm/qsharp/ux/circuit-vis/register.ts | 9 + npm/qsharp/ux/circuit-vis/sqore.ts | 376 ++++++++++++ npm/qsharp/ux/circuit-vis/styles.ts | 236 ++++++++ npm/qsharp/ux/circuit-vis/utils.ts | 77 +++ npm/qsharp/ux/circuit.tsx | 4 +- npm/qsharp/ux/data.ts | 2 +- package-lock.json | 7 - package.json | 1 - vscode/src/circuit.ts | 2 +- 26 files changed, 2466 insertions(+), 15 deletions(-) create mode 100644 npm/qsharp/src/shared/README.md create mode 100644 npm/qsharp/src/shared/circuit.ts create mode 100644 npm/qsharp/src/shared/register.ts create mode 100644 npm/qsharp/ux/circuit-vis/README.md create mode 100644 npm/qsharp/ux/circuit-vis/circuit.ts create mode 100644 npm/qsharp/ux/circuit-vis/constants.ts create mode 100644 npm/qsharp/ux/circuit-vis/formatters/formatUtils.ts create mode 100644 npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts create mode 100644 npm/qsharp/ux/circuit-vis/formatters/inputFormatter.ts create mode 100644 npm/qsharp/ux/circuit-vis/formatters/registerFormatter.ts create mode 100644 npm/qsharp/ux/circuit-vis/index.ts create mode 100644 npm/qsharp/ux/circuit-vis/metadata.ts create mode 100644 npm/qsharp/ux/circuit-vis/process.ts create mode 100644 npm/qsharp/ux/circuit-vis/register.ts create mode 100644 npm/qsharp/ux/circuit-vis/sqore.ts create mode 100644 npm/qsharp/ux/circuit-vis/styles.ts create mode 100644 npm/qsharp/ux/circuit-vis/utils.ts diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 508845f3e0..47bd66fa10 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -9,7 +9,7 @@ use serde::Serialize; use std::{fmt::Display, fmt::Write, ops::Not, vec}; /// Representation of a quantum circuit. -/// Implementation of +/// Implementation of `CircuitData` type from `qsharp-lang` npm package. #[derive(Clone, Serialize, Default, Debug, PartialEq)] pub struct Circuit { pub operations: Vec, diff --git a/npm/qsharp/src/browser.ts b/npm/qsharp/src/browser.ts index 607b0116df..c49647e001 100644 --- a/npm/qsharp/src/browser.ts +++ b/npm/qsharp/src/browser.ts @@ -191,3 +191,5 @@ export type { }; export * as utils from "./utils.js"; + +export type { Circuit as CircuitData } from "./shared/circuit.js"; diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 453984c7b1..4558911523 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { type Circuit as CircuitData } from "@microsoft/quantum-viz.js/lib/circuit.js"; +import { type Circuit as CircuitData } from "../shared/circuit.js"; import { IDocFile, IOperationInfo, diff --git a/npm/qsharp/src/debug-service/debug-service.ts b/npm/qsharp/src/debug-service/debug-service.ts index 151d7132e0..b5ed11c1bb 100644 --- a/npm/qsharp/src/debug-service/debug-service.ts +++ b/npm/qsharp/src/debug-service/debug-service.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { type Circuit as CircuitData } from "@microsoft/quantum-viz.js/lib/circuit.js"; +import { type Circuit as CircuitData } from "../shared/circuit.js"; import type { DebugService, IBreakpointSpan, diff --git a/npm/qsharp/src/shared/README.md b/npm/qsharp/src/shared/README.md new file mode 100644 index 0000000000..79f7c19f16 --- /dev/null +++ b/npm/qsharp/src/shared/README.md @@ -0,0 +1 @@ +This directory contains shared modules to be referenced from both `qsharp/src/` and `qsharp/ux/`. diff --git a/npm/qsharp/src/shared/circuit.ts b/npm/qsharp/src/shared/circuit.ts new file mode 100644 index 0000000000..6b31e1926b --- /dev/null +++ b/npm/qsharp/src/shared/circuit.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Register } from "./register.js"; + +/** + * Circuit to be visualized. + */ +export interface Circuit { + /** Array of qubit resources. */ + qubits: Qubit[]; + operations: Operation[]; +} + +/** + * Represents a unique qubit resource bit. + */ +export interface Qubit { + /** Qubit ID. */ + id: number; + /** Number of classical registers attached to quantum register. */ + numChildren?: number; +} + +/** + * Conditions on when to render the given operation. + */ +export enum ConditionalRender { + /** Always rendered. */ + Always, + /** Render classically-controlled operation when measurement is a zero. */ + OnZero, + /** Render classically-controlled operation when measurement is a one. */ + OnOne, + /** Render operation as a group of its nested operations. */ + AsGroup, +} + +/** + * Custom data attributes (e.g. data-{attr}="{val}") + */ +export interface DataAttributes { + [attr: string]: string; +} + +/** + * Represents an operation and the registers it acts on. + */ +export interface Operation { + /** Gate label. */ + gate: string; + /** Formatted gate arguments to be displayed. */ + displayArgs?: string; + /** Nested operations within this operation. */ + children?: Operation[]; + /** Whether gate is a measurement operation. */ + isMeasurement: boolean; + /** Whether gate is a conditional operation. */ + isConditional: boolean; + /** Whether gate is a controlled operation. */ + isControlled: boolean; + /** Whether gate is an adjoint operation. */ + isAdjoint: boolean; + /** Control registers the gate acts on. */ + controls?: Register[]; + /** Target registers the gate acts on. */ + targets: Register[]; + /** Specify conditions on when to render operation. */ + conditionalRender?: ConditionalRender; + /** Custom data attributes to attach to gate element. */ + dataAttributes?: DataAttributes; +} diff --git a/npm/qsharp/src/shared/register.ts b/npm/qsharp/src/shared/register.ts new file mode 100644 index 0000000000..e2fb0588fa --- /dev/null +++ b/npm/qsharp/src/shared/register.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Type of register. + */ +export enum RegisterType { + Qubit, + Classical, +} + +/** + * Represents a register resource. + */ +export interface Register { + /** Type of register. If missing defaults to Qubit. */ + type?: RegisterType; + /** Qubit register ID. */ + qId: number; + /** Classical register ID (if classical register). */ + cId?: number; +} + +/** + * Metadata for qubit register. + */ +export interface RegisterMetadata { + /** Type of register. */ + type: RegisterType; + /** y coord of register */ + y: number; + /** Nested classical registers attached to quantum register. */ + children?: RegisterMetadata[]; +} + +/** + * Mapping from qubit IDs to their register metadata. + */ +export interface RegisterMap { + [id: number]: RegisterMetadata; +} diff --git a/npm/qsharp/ux/circuit-vis/README.md b/npm/qsharp/ux/circuit-vis/README.md new file mode 100644 index 0000000000..0a33f95dc0 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/README.md @@ -0,0 +1 @@ +The code in this folder was copied and adapted from `https://github.com/microsoft/quantum-viz.js`. diff --git a/npm/qsharp/ux/circuit-vis/circuit.ts b/npm/qsharp/ux/circuit-vis/circuit.ts new file mode 100644 index 0000000000..aff04aa9f1 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/circuit.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { + ConditionalRender, + type Circuit, + type DataAttributes, + type Operation, + type Qubit, +} from "../../src/shared/circuit"; diff --git a/npm/qsharp/ux/circuit-vis/constants.ts b/npm/qsharp/ux/circuit-vis/constants.ts new file mode 100644 index 0000000000..fe2763319c --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/constants.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// SVG Namespace +export const svgNS = "http://www.w3.org/2000/svg"; + +// Display attributes +/** Left padding of SVG. */ +export const leftPadding = 20; +/** x coordinate for first operation on each register. */ +export const startX = 80; +/** y coordinate of first register. */ +export const startY = 40; +/** Minimum width of each gate. */ +export const minGateWidth = 40; +/** Height of each gate. */ +export const gateHeight = 40; +/** Padding on each side of gate. */ +export const gatePadding = 10; +/** Padding on each side of gate label. */ +export const labelPadding = 10; +/** Height between each qubit register. */ +export const registerHeight: number = gateHeight + gatePadding * 2; +/** Height between classical registers. */ +export const classicalRegHeight: number = gateHeight; +/** Group box inner padding. */ +export const groupBoxPadding = gatePadding; +/** Padding between nested groups. */ +export const nestedGroupPadding = 2; +/** Additional offset for control button. */ +export const controlBtnOffset = 40; +/** Control button radius. */ +export const controlBtnRadius = 15; +/** Default font size for gate labels. */ +export const labelFontSize = 14; +/** Default font size for gate arguments. */ +export const argsFontSize = 12; +/** Starting x coord for each register wire. */ +export const regLineStart = 40; diff --git a/npm/qsharp/ux/circuit-vis/formatters/formatUtils.ts b/npm/qsharp/ux/circuit-vis/formatters/formatUtils.ts new file mode 100644 index 0000000000..c3bccd1beb --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/formatters/formatUtils.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { labelFontSize, svgNS } from "../constants"; + +// Helper functions for basic SVG components + +/** + * Create an SVG element. + * + * @param type The type of element to be created. + * @param attributes The attributes that define the element. + * + * @returns SVG element. + */ +export const createSvgElement = ( + type: string, + attributes: { [attr: string]: string } = {}, +): SVGElement => { + const el: SVGElement = document.createElementNS(svgNS, type); + Object.entries(attributes).forEach(([attrName, attrVal]) => + el.setAttribute(attrName, attrVal), + ); + return el; +}; + +/** + * Given an array of SVG elements, group them as an SVG group using the `` tag. + * + * @param svgElems Array of SVG elements. + * @param attributes Key-value pairs of attributes and they values. + * + * @returns SVG element for grouped elements. + */ +export const group = ( + svgElems: SVGElement[], + attributes: { [attr: string]: string } = {}, +): SVGElement => { + const el: SVGElement = createSvgElement("g", attributes); + svgElems.forEach((child: SVGElement) => el.appendChild(child)); + return el; +}; + +/** + * Generate an SVG line. + * + * @param x1 x coord of starting point of line. + * @param y1 y coord of starting point of line. + * @param x2 x coord of ending point of line. + * @param y2 y coord fo ending point of line. + * @param className Class name of element. + * + * @returns SVG element for line. + */ +export const line = ( + x1: number, + y1: number, + x2: number, + y2: number, + className?: string, +): SVGElement => { + const attrs: { [attr: string]: string } = { + x1: x1.toString(), + x2: x2.toString(), + y1: y1.toString(), + y2: y2.toString(), + }; + if (className != null) attrs["class"] = className; + return createSvgElement("line", attrs); +}; + +/** + * Generate an SVG circle. + * + * @param x x coord of circle. + * @param y y coord of circle. + * @param radius Radius of circle. + * + * @returns SVG element for circle. + */ +export const circle = ( + x: number, + y: number, + radius: number, + className?: string, +): SVGElement => { + const attrs: { [attr: string]: string } = { + cx: x.toString(), + cy: y.toString(), + r: radius.toString(), + }; + if (className != null) attrs["class"] = className; + return createSvgElement("circle", attrs); +}; + +/** + * Generate the SVG representation of a control dot used for controlled operations. + * + * @param x x coord of circle. + * @param y y coord of circle. + * @param radius Radius of circle. + * + * @returns SVG element for control dot. + */ +export const controlDot = (x: number, y: number, radius = 5): SVGElement => + circle(x, y, radius, "control-dot"); + +/** + * Generate the SVG representation of a unitary box that represents an arbitrary unitary operation. + * + * @param x x coord of box. + * @param y y coord of box. + * @param width Width of box. + * @param height Height of box. + * @param className Class name of element. + * + * @returns SVG element for unitary box. + */ +export const box = ( + x: number, + y: number, + width: number, + height: number, + className = "gate-unitary", +): SVGElement => + createSvgElement("rect", { + class: className, + x: x.toString(), + y: y.toString(), + width: width.toString(), + height: height.toString(), + }); + +/** + * Generate the SVG text element from a given text string. + * + * @param text String to render as SVG text. + * @param x Middle x coord of text. + * @param y Middle y coord of text. + * @param fs Font size of text. + * + * @returns SVG element for text. + */ +export const text = ( + text: string, + x: number, + y: number, + fs: number = labelFontSize, +): SVGElement => { + const el: SVGElement = createSvgElement("text", { + "font-size": fs.toString(), + x: x.toString(), + y: y.toString(), + }); + el.textContent = text; + return el; +}; + +/** + * Generate the SVG representation of the arc used in the measurement box. + * + * @param x x coord of arc. + * @param y y coord of arc. + * @param rx x radius of arc. + * @param ry y radius of arc. + * + * @returns SVG element for arc. + */ +export const arc = (x: number, y: number, rx: number, ry: number): SVGElement => + createSvgElement("path", { + class: "arc-measure", + d: `M ${x + 2 * rx} ${y} A ${rx} ${ry} 0 0 0 ${x} ${y}`, + }); + +/** + * Generate a dashed SVG line. + * + * @param x1 x coord of starting point of line. + * @param y1 y coord of starting point of line. + * @param x2 x coord of ending point of line. + * @param y2 y coord fo ending point of line. + * @param className Class name of element. + * + * @returns SVG element for dashed line. + */ +export const dashedLine = ( + x1: number, + y1: number, + x2: number, + y2: number, + className?: string, +): SVGElement => { + const el: SVGElement = line(x1, y1, x2, y2, className); + el.setAttribute("stroke-dasharray", "8, 8"); + return el; +}; + +/** + * Generate the SVG representation of the dashed box used for enclosing groups of operations controlled on a classical register. + * + * @param x x coord of box. + * @param y y coord of box. + * @param width Width of box. + * @param height Height of box. + * @param className Class name of element. + * + * @returns SVG element for dashed box. + */ +export const dashedBox = ( + x: number, + y: number, + width: number, + height: number, + className?: string, +): SVGElement => { + const el: SVGElement = box(x, y, width, height, className); + el.setAttribute("fill-opacity", "0"); + el.setAttribute("stroke-dasharray", "8, 8"); + return el; +}; diff --git a/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts b/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts new file mode 100644 index 0000000000..f4be3626df --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts @@ -0,0 +1,557 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Metadata, GateType } from "../metadata"; +import { + minGateWidth, + gateHeight, + labelFontSize, + argsFontSize, + controlBtnRadius, + controlBtnOffset, + groupBoxPadding, + classicalRegHeight, + nestedGroupPadding, +} from "../constants"; +import { + createSvgElement, + group, + line, + circle, + controlDot, + box, + text, + arc, + dashedLine, + dashedBox, +} from "./formatUtils"; + +/** + * Given an array of operations (in metadata format), return the SVG representation. + * + * @param opsMetadata Array of Metadata representation of operations. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of operations. + */ +const formatGates = (opsMetadata: Metadata[], nestedDepth = 0): SVGElement => { + const formattedGates: SVGElement[] = opsMetadata.map((metadata) => + _formatGate(metadata, nestedDepth), + ); + return group(formattedGates); +}; + +/** + * Takes in an operation's metadata and formats it into SVG. + * + * @param metadata Metadata object representation of gate. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of gate. + */ +const _formatGate = (metadata: Metadata, nestedDepth = 0): SVGElement => { + const { type, x, controlsY, targetsY, label, displayArgs, width } = metadata; + switch (type) { + case GateType.Measure: + return _createGate([_measure(x, controlsY[0])], metadata, nestedDepth); + case GateType.Unitary: + return _createGate( + [_unitary(label, x, targetsY as number[][], width, displayArgs)], + metadata, + nestedDepth, + ); + case GateType.X: + return _createGate([_x(metadata, nestedDepth)], metadata, nestedDepth); + case GateType.Swap: + return controlsY.length > 0 + ? _controlledGate(metadata, nestedDepth) + : _createGate([_swap(metadata, nestedDepth)], metadata, nestedDepth); + case GateType.Cnot: + case GateType.ControlledUnitary: + return _controlledGate(metadata, nestedDepth); + case GateType.Group: + return _groupedOperations(metadata, nestedDepth); + case GateType.ClassicalControlled: + return _classicalControlled(metadata); + default: + throw new Error(`ERROR: unknown gate (${label}) of type ${type}.`); + } +}; + +/** + * Groups SVG elements into a gate SVG group. + * + * @param svgElems Array of SVG elements. + * @param dataAttributes Custom data attributes to be attached to SVG group. + * + * @returns SVG representation of a gate. + */ +const _createGate = ( + svgElems: SVGElement[], + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const { dataAttributes } = metadata || {}; + const attributes: { [attr: string]: string } = { class: "gate" }; + Object.entries(dataAttributes || {}).forEach( + ([attr, val]) => (attributes[`data-${attr}`] = val), + ); + + const zoomBtn: SVGElement | null = _zoomButton(metadata, nestedDepth); + if (zoomBtn != null) svgElems = svgElems.concat([zoomBtn]); + return group(svgElems, attributes); +}; + +/** + * Returns the expand/collapse button for an operation if it can be zoomed-in or zoomed-out, + * respectively. If neither are allowed, return `null`. + * + * @param metadata Operation metadata. + * @param nestedDepth Depth of nested operation. + * + * @returns SVG element for expand/collapse button if needed, or null otherwise. + */ +const _zoomButton = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement | null => { + if (metadata == undefined) return null; + + const [x1, y1] = _gatePosition(metadata, nestedDepth); + let { dataAttributes } = metadata; + dataAttributes = dataAttributes || {}; + + const expanded = "expanded" in dataAttributes; + + const x = x1 + 2; + const y = y1 + 2; + const circleBorder: SVGElement = circle(x, y, 10); + + if (expanded) { + // Create collapse button if expanded + const minusSign: SVGElement = createSvgElement("path", { + d: `M${x - 7},${y} h14`, + }); + const elements: SVGElement[] = [circleBorder, minusSign]; + return group(elements, { class: "gate-control gate-collapse" }); + } else if (dataAttributes["zoom-in"] == "true") { + // Create expand button if operation can be zoomed in + const plusSign: SVGElement = createSvgElement("path", { + d: `M${x},${y - 7} v14 M${x - 7},${y} h14`, + }); + const elements: SVGElement[] = [circleBorder, plusSign]; + return group(elements, { class: "gate-control gate-expand" }); + } + + return null; +}; + +/** + * Calculate position of gate. + * + * @param metadata Operation metadata. + * @param nestedDepth Depth of nested operations. + * + * @returns Coordinates of gate: [x1, y1, x2, y2]. + */ +const _gatePosition = ( + metadata: Metadata, + nestedDepth: number, +): [number, number, number, number] => { + const { x, width, type, targetsY } = metadata; + + const ys = targetsY?.flatMap((y) => y as number[]) || []; + const maxY = Math.max(...ys); + const minY = Math.min(...ys); + + let x1: number, y1: number, x2: number, y2: number; + + switch (type) { + case GateType.Group: { + const padding = groupBoxPadding - nestedDepth * nestedGroupPadding; + + x1 = x - 2 * padding; + y1 = minY - gateHeight / 2 - padding; + x2 = width + 2 * padding; + y2 = maxY + +gateHeight / 2 + padding - (minY - gateHeight / 2 - padding); + + return [x1, y1, x2, y2]; + } + + default: + x1 = x - width / 2; + y1 = minY - gateHeight / 2; + x2 = x + width; + y2 = maxY + gateHeight / 2; + } + + return [x1, y1, x2, y2]; +}; + +/** + * Creates a measurement gate at position (x, y). + * + * @param x x coord of measurement gate. + * @param y y coord of measurement gate. + * + * @returns SVG representation of measurement gate. + */ +const _measure = (x: number, y: number): SVGElement => { + x -= minGateWidth / 2; + const width: number = minGateWidth, + height = gateHeight; + // Draw measurement box + const mBox: SVGElement = box( + x, + y - height / 2, + width, + height, + "gate-measure", + ); + const mArc: SVGElement = arc(x + 5, y + 2, width / 2 - 5, height / 2 - 8); + const meter: SVGElement = line( + x + width / 2, + y + 8, + x + width - 8, + y - height / 2 + 8, + ); + return group([mBox, mArc, meter]); +}; + +/** + * Creates the SVG for a unitary gate on an arbitrary number of qubits. + * + * @param label Gate label. + * @param x x coord of gate. + * @param y Array of y coords of registers acted upon by gate. + * @param width Width of gate. + * @param displayArgs Arguments passed in to gate. + * @param renderDashedLine If true, draw dashed lines between non-adjacent unitaries. + * + * @returns SVG representation of unitary gate. + */ +const _unitary = ( + label: string, + x: number, + y: number[][], + width: number, + displayArgs?: string, + renderDashedLine = true, +): SVGElement => { + if (y.length === 0) + throw new Error( + `Failed to render unitary gate (${label}): has no y-values`, + ); + + // Render each group as a separate unitary boxes + const unitaryBoxes: SVGElement[] = y.map((group: number[]) => { + const maxY: number = group[group.length - 1], + minY: number = group[0]; + const height: number = maxY - minY + gateHeight; + return _unitaryBox(label, x, minY, width, height, displayArgs); + }); + + // Draw dashed line between disconnected unitaries + if (renderDashedLine && unitaryBoxes.length > 1) { + const lastBox: number[] = y[y.length - 1]; + const firstBox: number[] = y[0]; + const maxY: number = lastBox[lastBox.length - 1], + minY: number = firstBox[0]; + const vertLine: SVGElement = dashedLine(x, minY, x, maxY); + return group([vertLine, ...unitaryBoxes]); + } + + return group(unitaryBoxes); +}; + +/** + * Generates SVG representation of the boxed unitary gate symbol. + * + * @param label Label for unitary operation. + * @param x x coord of gate. + * @param y y coord of gate. + * @param width Width of gate. + * @param height Height of gate. + * @param displayArgs Arguments passed in to gate. + * + * @returns SVG representation of unitary box. + */ +const _unitaryBox = ( + label: string, + x: number, + y: number, + width: number, + height: number = gateHeight, + displayArgs?: string, +): SVGElement => { + y -= gateHeight / 2; + const uBox: SVGElement = box(x - width / 2, y, width, height); + const labelY = y + height / 2 - (displayArgs == null ? 0 : 7); + const labelText: SVGElement = text(label, x, labelY); + const elems = [uBox, labelText]; + if (displayArgs != null) { + const argStrY = y + height / 2 + 8; + const argText: SVGElement = text(displayArgs, x, argStrY, argsFontSize); + elems.push(argText); + } + return group(elems); +}; + +/** + * Creates the SVG for a SWAP gate on y coords given by targetsY. + * + * @param x Centre x coord of SWAP gate. + * @param targetsY y coords of target registers. + * + * @returns SVG representation of SWAP gate. + */ +const _swap = (metadata: Metadata, nestedDepth: number): SVGElement => { + const { x, targetsY } = metadata; + + // Get SVGs of crosses + const [x1, y1, x2, y2] = _gatePosition(metadata, nestedDepth); + const ys = targetsY?.flatMap((y) => y as number[]) || []; + + const bg: SVGElement = box(x1, y1, x2, y2, "gate-swap"); + const crosses: SVGElement[] = ys.map((y) => _cross(x, y)); + const vertLine: SVGElement = line(x, ys[0], x, ys[1]); + return group([bg, ...crosses, vertLine]); +}; +/** + * Creates the SVG for an X gate + * + * @returns SVG representation of X gate. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _x = (metadata: Metadata, _: number): SVGElement => { + const { x, targetsY } = metadata; + const ys = targetsY.flatMap((y) => y as number[]); + return _oplus(x, ys[0]); +}; +/** + * Generates cross for display in SWAP gate. + * + * @param x x coord of gate. + * @param y y coord of gate. + * + * @returns SVG representation for cross. + */ +const _cross = (x: number, y: number): SVGElement => { + const radius = 8; + const line1: SVGElement = line( + x - radius, + y - radius, + x + radius, + y + radius, + ); + const line2: SVGElement = line( + x - radius, + y + radius, + x + radius, + y - radius, + ); + return group([line1, line2]); +}; + +/** + * Produces the SVG representation of a controlled gate on multiple qubits. + * + * @param metadata Metadata of controlled gate. + * + * @returns SVG representation of controlled gate. + */ +const _controlledGate = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const targetGateSvgs: SVGElement[] = []; + const { type, x, controlsY, label, displayArgs, width } = metadata; + let { targetsY } = metadata; + + // Get SVG for target gates + switch (type) { + case GateType.Cnot: + (targetsY as number[]).forEach((y) => targetGateSvgs.push(_oplus(x, y))); + break; + case GateType.Swap: + (targetsY as number[]).forEach((y) => targetGateSvgs.push(_cross(x, y))); + break; + case GateType.ControlledUnitary: + { + const groupedTargetsY: number[][] = targetsY as number[][]; + targetGateSvgs.push( + _unitary(label, x, groupedTargetsY, width, displayArgs, false), + ); + targetsY = targetsY.flat(); + } + break; + default: + throw new Error(`ERROR: Unrecognized gate: ${label} of type ${type}`); + } + // Get SVGs for control dots + const controlledDotsSvg: SVGElement[] = controlsY.map((y) => + controlDot(x, y), + ); + // Create control lines + const maxY: number = Math.max(...controlsY, ...(targetsY as number[])); + const minY: number = Math.min(...controlsY, ...(targetsY as number[])); + const vertLine: SVGElement = line(x, minY, x, maxY); + const svg: SVGElement = _createGate( + [vertLine, ...controlledDotsSvg, ...targetGateSvgs], + metadata, + nestedDepth, + ); + return svg; +}; + +/** + * Generates $\oplus$ symbol for display in CNOT gate. + * + * @param x x coordinate of gate. + * @param y y coordinate of gate. + * @param r radius of circle. + * + * @returns SVG representation of $\oplus$ symbol. + */ +const _oplus = (x: number, y: number, r = 15): SVGElement => { + const circleBorder: SVGElement = circle(x, y, r); + const vertLine: SVGElement = line(x, y - r, x, y + r); + const horLine: SVGElement = line(x - r, y, x + r, y); + return group([circleBorder, vertLine, horLine], { class: "oplus" }); +}; + +/** + * Generates the SVG for a group of nested operations. + * + * @param metadata Metadata representation of gate. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of gate. + */ +const _groupedOperations = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const { children } = metadata; + const [x1, y1, x2, y2] = _gatePosition(metadata, nestedDepth); + + // Draw dashed box around children gates + const box: SVGElement = dashedBox(x1, y1, x2, y2); + const elems: SVGElement[] = [box]; + if (children != null) + elems.push(formatGates(children as Metadata[], nestedDepth + 1)); + return _createGate(elems, metadata, nestedDepth); +}; + +/** + * Generates the SVG for a classically controlled group of operations. + * + * @param metadata Metadata representation of gate. + * @param padding Padding within dashed box. + * + * @returns SVG representation of gate. + */ +const _classicalControlled = ( + metadata: Metadata, + padding: number = groupBoxPadding, +): SVGElement => { + const { controlsY, dataAttributes } = metadata; + const targetsY: number[] = metadata.targetsY as number[]; + const children: Metadata[][] = metadata.children as Metadata[][]; + let { x, width } = metadata; + + const controlY = controlsY[0]; + + const elems: SVGElement[] = []; + + if (children != null) { + if (children.length !== 2) + throw new Error( + `Invalid number of children found for classically-controlled gate: ${children.length}`, + ); + + // Get SVG for gates controlled on 0 + const childrenZero: SVGElement = formatGates(children[0]); + childrenZero.setAttribute("class", "gates-zero"); + elems.push(childrenZero); + + // Get SVG for gates controlled on 1 + const childrenOne: SVGElement = formatGates(children[1]); + childrenOne.setAttribute("class", "gates-one"); + elems.push(childrenOne); + } + + // Draw control button and attached dashed line to dashed box + const controlCircleX: number = x + controlBtnRadius; + const controlCircle: SVGElement = _controlCircle(controlCircleX, controlY); + const lineY1: number = controlY + controlBtnRadius, + lineY2: number = controlY + classicalRegHeight / 2; + const vertLine: SVGElement = dashedLine( + controlCircleX, + lineY1, + controlCircleX, + lineY2, + "classical-line", + ); + x += controlBtnOffset; + const horLine: SVGElement = dashedLine( + controlCircleX, + lineY2, + x, + lineY2, + "classical-line", + ); + + width = width - controlBtnOffset + (padding - groupBoxPadding) * 2; + x += groupBoxPadding - padding; + const y: number = targetsY[0] - gateHeight / 2 - padding; + const height: number = targetsY[1] - targetsY[0] + gateHeight + padding * 2; + + // Draw dashed box around children gates + const box: SVGElement = dashedBox(x, y, width, height, "classical-container"); + + elems.push(...[horLine, vertLine, controlCircle, box]); + + // Display controlled operation in initial "unknown" state + const attributes: { [attr: string]: string } = { + class: `classically-controlled-group classically-controlled-unknown`, + }; + if (dataAttributes != null) + Object.entries(dataAttributes).forEach( + ([attr, val]) => (attributes[`data-${attr}`] = val), + ); + + return group(elems, attributes); +}; + +/** + * Generates the SVG representation of the control circle on a classical register with interactivity support + * for toggling between bit values (unknown, 1, and 0). + * + * @param x x coord. + * @param y y coord. + * @param r Radius of circle. + * + * @returns SVG representation of control circle. + */ +const _controlCircle = ( + x: number, + y: number, + r: number = controlBtnRadius, +): SVGElement => + group([circle(x, y, r), text("?", x, y, labelFontSize)], { + class: "classically-controlled-btn", + }); + +export { + formatGates, + _formatGate, + _createGate, + _zoomButton, + _measure, + _unitary, + _swap, + _controlledGate, + _groupedOperations, + _classicalControlled, +}; diff --git a/npm/qsharp/ux/circuit-vis/formatters/inputFormatter.ts b/npm/qsharp/ux/circuit-vis/formatters/inputFormatter.ts new file mode 100644 index 0000000000..7464de4c33 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/formatters/inputFormatter.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Qubit } from "../circuit"; +import { RegisterType, RegisterMap, RegisterMetadata } from "../register"; +import { + leftPadding, + startY, + registerHeight, + classicalRegHeight, +} from "../constants"; +import { group, text } from "./formatUtils"; + +/** + * `formatInputs` takes in an array of Qubits and outputs the SVG string of formatted + * qubit wires and a mapping from register IDs to register metadata (for rendering). + * + * @param qubits List of declared qubits. + * + * @returns returns the SVG string of formatted qubit wires, a mapping from registers + * to y coord and total SVG height. + */ +const formatInputs = ( + qubits: Qubit[], +): { qubitWires: SVGElement; registers: RegisterMap; svgHeight: number } => { + const qubitWires: SVGElement[] = []; + const registers: RegisterMap = {}; + + let currY: number = startY; + qubits.forEach(({ id, numChildren }) => { + // Add qubit wire to list of qubit wires + qubitWires.push(_qubitInput(currY)); + + // Create qubit register + registers[id] = { type: RegisterType.Qubit, y: currY }; + + // If there are no attached classical registers, increment y by fixed register height + if (numChildren == null || numChildren === 0) { + currY += registerHeight; + return; + } + + // Increment current height by classical register height for attached classical registers + currY += classicalRegHeight; + + // Add classical wires + registers[id].children = Array.from(Array(numChildren), () => { + const clsReg: RegisterMetadata = { + type: RegisterType.Classical, + y: currY, + }; + currY += classicalRegHeight; + return clsReg; + }); + }); + + return { + qubitWires: group(qubitWires), + registers, + svgHeight: currY, + }; +}; + +/** + * Generate the SVG text component for the input qubit register. + * + * @param y y coord of input wire to render in SVG. + * + * @returns SVG text component for the input register. + */ +const _qubitInput = (y: number): SVGElement => { + const el: SVGElement = text("|0⟩", leftPadding, y, 16); + el.setAttribute("text-anchor", "start"); + el.setAttribute("dominant-baseline", "middle"); + return el; +}; + +export { formatInputs, _qubitInput }; diff --git a/npm/qsharp/ux/circuit-vis/formatters/registerFormatter.ts b/npm/qsharp/ux/circuit-vis/formatters/registerFormatter.ts new file mode 100644 index 0000000000..aa858e9278 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/formatters/registerFormatter.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { RegisterMap } from "../register"; +import { regLineStart } from "../constants"; +import { Metadata, GateType } from "../metadata"; +import { group, line, text } from "./formatUtils"; + +/** + * Generate the SVG representation of the qubit register wires in `registers` and the classical wires + * stemming from each measurement gate. + * + * @param registers Map from register IDs to register metadata. + * @param measureGates Array of measurement gates metadata. + * @param endX End x coord. + * + * @returns SVG representation of register wires. + */ +const formatRegisters = ( + registers: RegisterMap, + measureGates: Metadata[], + endX: number, +): SVGElement => { + const formattedRegs: SVGElement[] = []; + // Render qubit wires + for (const qId in registers) { + formattedRegs.push(_qubitRegister(Number(qId), endX, registers[qId].y)); + } + // Render classical wires + measureGates.forEach(({ type, x, targetsY, controlsY }) => { + if (type !== GateType.Measure) return; + const gateY: number = controlsY[0]; + (targetsY as number[]).forEach((y) => { + formattedRegs.push(_classicalRegister(x, gateY, endX, y)); + }); + }); + return group(formattedRegs); +}; + +/** + * Generates the SVG representation of a classical register. + * + * @param startX Start x coord. + * @param gateY y coord of measurement gate. + * @param endX End x coord. + * @param wireY y coord of wire. + * + * @returns SVG representation of the given classical register. + */ +const _classicalRegister = ( + startX: number, + gateY: number, + endX: number, + wireY: number, +): SVGElement => { + const wirePadding = 1; + // Draw vertical lines + const vLine1: SVGElement = line( + startX + wirePadding, + gateY, + startX + wirePadding, + wireY - wirePadding, + "register-classical", + ); + const vLine2: SVGElement = line( + startX - wirePadding, + gateY, + startX - wirePadding, + wireY + wirePadding, + "register-classical", + ); + + // Draw horizontal lines + const hLine1: SVGElement = line( + startX + wirePadding, + wireY - wirePadding, + endX, + wireY - wirePadding, + "register-classical", + ); + const hLine2: SVGElement = line( + startX - wirePadding, + wireY + wirePadding, + endX, + wireY + wirePadding, + "register-classical", + ); + + return group([vLine1, vLine2, hLine1, hLine2]); +}; + +/** + * Generates the SVG representation of a qubit register. + * + * @param qId Qubit register index. + * @param endX End x coord. + * @param y y coord of wire. + * @param labelOffset y offset for wire label. + * + * @returns SVG representation of the given qubit register. + */ +const _qubitRegister = ( + qId: number, + endX: number, + y: number, + labelOffset = 16, +): SVGElement => { + const wire: SVGElement = line(regLineStart, y, endX, y); + + const label: SVGElement = text(`q${qId}`, regLineStart, y - labelOffset); + label.setAttribute("dominant-baseline", "hanging"); + label.setAttribute("text-anchor", "start"); + label.setAttribute("font-size", "75%"); + + return group([wire, label]); +}; + +export { formatRegisters, _classicalRegister, _qubitRegister }; diff --git a/npm/qsharp/ux/circuit-vis/index.ts b/npm/qsharp/ux/circuit-vis/index.ts new file mode 100644 index 0000000000..5e37c199f8 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/index.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Sqore } from "./sqore"; +import { Circuit } from "./circuit"; +import { StyleConfig } from "./styles"; + +/** + * Render `circuit` into `container` at the specified layer depth. + * + * @param circuit Circuit to be visualized. + * @param container HTML element for rendering visualization into. + * @param style Custom visualization style. + * @param renderDepth Initial layer depth at which to render gates. + */ +export const draw = ( + circuit: Circuit, + container: HTMLElement, + style: StyleConfig | string = {}, + renderDepth = 0, +): void => { + const sqore = new Sqore(circuit, style); + sqore.draw(container, renderDepth); +}; + +export { STYLES } from "./styles"; + +// Export types +export type { StyleConfig } from "./styles"; +export type { Circuit, Qubit, Operation } from "./circuit"; diff --git a/npm/qsharp/ux/circuit-vis/metadata.ts b/npm/qsharp/ux/circuit-vis/metadata.ts new file mode 100644 index 0000000000..4c5c265289 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/metadata.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { DataAttributes } from "./circuit"; + +/** + * Enum for the various gate operations handled. + */ +export enum GateType { + /** Measurement gate. */ + Measure, + /** CNOT gate. */ + Cnot, + /** SWAP gate. */ + Swap, + /** X gate. */ + X, + /** Single/multi qubit unitary gate. */ + Unitary, + /** Single/multi controlled unitary gate. */ + ControlledUnitary, + /** Nested group of classically-controlled gates. */ + ClassicalControlled, + /** Group of nested gates */ + Group, + /** Invalid gate. */ + Invalid, +} + +/** + * Metadata used to store information pertaining to a given + * operation for rendering its corresponding SVG. + */ +export interface Metadata { + /** Gate type. */ + type: GateType; + /** Centre x coord for gate position. */ + x: number; + /** Array of y coords of control registers. */ + controlsY: number[]; + /** Array of y coords of target registers. + * For `GateType.Unitary` or `GateType.ControlledUnitary`, this is an array of groups of + * y coords, where each group represents a unitary box to be rendered separately. + */ + targetsY: (number | number[])[]; + /** Gate label. */ + label: string; + /** Gate arguments as string. */ + displayArgs?: string; + /** Gate width. */ + width: number; + /** Children operations as part of group. */ + children?: (Metadata | Metadata[])[]; + /** Custom data attributes to attach to gate element. */ + dataAttributes?: DataAttributes; +} diff --git a/npm/qsharp/ux/circuit-vis/process.ts b/npm/qsharp/ux/circuit-vis/process.ts new file mode 100644 index 0000000000..f18d7be1cf --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/process.ts @@ -0,0 +1,536 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + minGateWidth, + startX, + gatePadding, + controlBtnOffset, + groupBoxPadding, +} from "./constants"; +import { Operation, ConditionalRender } from "./circuit"; +import { Metadata, GateType } from "./metadata"; +import { Register, RegisterMap, RegisterType } from "./register"; +import { getGateWidth } from "./utils"; + +/** + * Takes in a list of operations and maps them to `metadata` objects which + * contains information for formatting the corresponding SVG. + * + * @param operations Array of operations. + * @param registers Mapping from qubit IDs to register metadata. + * + * @returns An object containing `metadataList` (Array of Metadata objects) and + * `svgWidth` which is the width of the entire SVG. + */ +const processOperations = ( + operations: Operation[], + registers: RegisterMap, +): { metadataList: Metadata[]; svgWidth: number } => { + if (operations.length === 0) return { metadataList: [], svgWidth: startX }; + + // Group operations based on registers + const groupedOps: number[][] = _groupOperations(operations, registers); + + // Align operations on multiple registers + const alignedOps: (number | null)[][] = _alignOps(groupedOps); + + // Maintain widths of each column to account for variable-sized gates + const numColumns: number = Math.max( + 0, + ...alignedOps.map((ops) => ops.length), + ); + const columnsWidths: number[] = new Array(numColumns).fill(minGateWidth); + + // Get classical registers and their starting column index + const classicalRegs: [number, Register][] = _getClassicalRegStart( + operations, + alignedOps, + ); + + // Keep track of which ops are already seen to avoid duplicate rendering + const visited: { [opIdx: number]: boolean } = {}; + + // Map operation index to gate metadata for formatting later + const opsMetadata: Metadata[][] = alignedOps.map((regOps) => + regOps.map((opIdx, col) => { + let op: Operation | null = null; + + // eslint-disable-next-line no-prototype-builtins + if (opIdx != null && !visited.hasOwnProperty(opIdx)) { + op = operations[opIdx]; + visited[opIdx] = true; + } + + const metadata: Metadata = _opToMetadata(op, registers); + + if ( + op != null && + [GateType.Unitary, GateType.ControlledUnitary].includes(metadata.type) + ) { + // If gate is a unitary type, split targetsY into groups if there + // is a classical register between them for rendering + + // Get y coordinates of classical registers in the same column as this operation + const classicalRegY: number[] = classicalRegs + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([regCol, _]) => regCol <= col) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(([_, reg]) => { + if (reg.cId == null) + throw new Error("Could not find cId for classical register."); + const { children } = registers[reg.qId]; + if (children == null) + throw new Error( + `Failed to find classical registers for qubit ID ${reg.qId}.`, + ); + return children[reg.cId].y; + }); + + metadata.targetsY = _splitTargetsY( + op.targets, + classicalRegY, + registers, + ); + } + + // Expand column size, if needed + if (metadata.width > columnsWidths[col]) { + columnsWidths[col] = metadata.width; + } + + return metadata; + }), + ); + + // Fill in x coord of each gate + const endX: number = _fillMetadataX(opsMetadata, columnsWidths); + + // Flatten operations and filter out invalid gates + const metadataList: Metadata[] = opsMetadata + .flat() + .filter(({ type }) => type != GateType.Invalid); + + return { metadataList, svgWidth: endX }; +}; + +/** + * Group gates provided by operations into their respective registers. + * + * @param operations Array of operations. + * @param numRegs Total number of registers. + * + * @returns 2D array of indices where `groupedOps[i][j]` is the index of the operations + * at register `i` and column `j` (not yet aligned/padded). + */ +const _groupOperations = ( + operations: Operation[], + registers: RegisterMap, +): number[][] => { + // NOTE: We get the max ID instead of just number of keys because there can be a qubit ID that + // isn't acted upon and thus does not show up as a key in registers. + const numRegs: number = + Math.max(-1, ...Object.keys(registers).map(Number)) + 1; + const groupedOps: number[][] = Array.from(Array(numRegs), () => new Array(0)); + operations.forEach(({ targets, controls }, instrIdx) => { + const ctrls: Register[] = controls || []; + const qRegs: Register[] = [...ctrls, ...targets].filter( + ({ type }) => (type || RegisterType.Qubit) === RegisterType.Qubit, + ); + const qRegIdxList: number[] = qRegs.map(({ qId }) => qId); + const clsControls: Register[] = ctrls.filter( + ({ type }) => (type || RegisterType.Qubit) === RegisterType.Classical, + ); + const isClassicallyControlled: boolean = clsControls.length > 0; + if (!isClassicallyControlled && qRegs.length === 0) return; + // If operation is classically-controlled, pad all qubit registers. Otherwise, only pad + // the contiguous range of registers that it covers. + const minRegIdx: number = isClassicallyControlled + ? 0 + : Math.min(...qRegIdxList); + const maxRegIdx: number = isClassicallyControlled + ? numRegs - 1 + : Math.max(...qRegIdxList); + // Add operation also to registers that are in-between target registers + // so that other gates won't render in the middle. + for (let i = minRegIdx; i <= maxRegIdx; i++) { + groupedOps[i].push(instrIdx); + } + }); + return groupedOps; +}; + +/** + * Aligns operations by padding registers with `null`s to make sure that multiqubit + * gates are in the same column. + * e.g. ---[x]---[x]-- + * ----------|--- + * + * @param ops 2D array of operations. Each row represents a register + * and the operations acting on it (in-order). + * + * @returns 2D array of aligned operations padded with `null`s. + */ +const _alignOps = (ops: number[][]): (number | null)[][] => { + let maxNumOps: number = Math.max(0, ...ops.map((regOps) => regOps.length)); + let col = 0; + // Deep copy ops to be returned as paddedOps + const paddedOps: (number | null)[][] = JSON.parse(JSON.stringify(ops)); + while (col < maxNumOps) { + for (let regIdx = 0; regIdx < paddedOps.length; regIdx++) { + const reg: (number | null)[] = paddedOps[regIdx]; + if (reg.length <= col) continue; + + // Should never be null (nulls are only padded to previous columns) + const opIdx: number | null = reg[col]; + + // Get position of gate + const targetsPos: number[] = paddedOps.map((regOps) => + regOps.indexOf(opIdx), + ); + const gatePos: number = Math.max(-1, ...targetsPos); + + // If current column is not desired gate position, pad with null + if (col < gatePos) { + paddedOps[regIdx].splice(col, 0, null); + maxNumOps = Math.max(maxNumOps, paddedOps[regIdx].length); + } + } + col++; + } + return paddedOps; +}; + +/** + * Retrieves the starting index of each classical register. + * + * @param ops Array of operations. + * @param idxList 2D array of aligned operation indices. + * + * @returns Array of classical register and their starting column indices in the form [[column, register]]. + */ +const _getClassicalRegStart = ( + ops: Operation[], + idxList: (number | null)[][], +): [number, Register][] => { + const clsRegs: [number, Register][] = []; + idxList.forEach((reg) => { + for (let col = 0; col < reg.length; col++) { + const opIdx: number | null = reg[col]; + if (opIdx != null && ops[opIdx].isMeasurement) { + const targetClsRegs: Register[] = ops[opIdx].targets.filter( + (reg) => reg.type === RegisterType.Classical, + ); + targetClsRegs.forEach((reg) => clsRegs.push([col, reg])); + } + } + }); + return clsRegs; +}; + +/** + * Maps operation to metadata (e.g. gate type, position, dimensions, text) + * required to render the image. + * + * @param op Operation to be mapped into metadata format. + * @param registers Array of registers. + * + * @returns Metadata representation of given operation. + */ +const _opToMetadata = ( + op: Operation | null, + registers: RegisterMap, +): Metadata => { + const metadata: Metadata = { + type: GateType.Invalid, + x: 0, + controlsY: [], + targetsY: [], + label: "", + width: -1, + }; + + if (op == null) return metadata; + + const { + gate, + dataAttributes, + displayArgs, + isMeasurement, + isConditional, + isControlled, + isAdjoint, + controls, + targets, + children, + conditionalRender, + } = op; + + // Set y coords + metadata.controlsY = controls?.map((reg) => _getRegY(reg, registers)) || []; + metadata.targetsY = targets.map((reg) => _getRegY(reg, registers)); + + if (isConditional) { + // Classically-controlled operations + if (children == null || children.length == 0) + throw new Error( + "No children operations found for classically-controlled operation.", + ); + + // Gates to display when classical bit is 0. + const onZeroOps: Operation[] = children.filter( + (op) => op.conditionalRender !== ConditionalRender.OnOne, + ); + let childrenInstrs = processOperations(onZeroOps, registers); + const zeroGates: Metadata[] = childrenInstrs.metadataList; + const zeroChildWidth: number = childrenInstrs.svgWidth; + + // Gates to display when classical bit is 1. + const onOneOps: Operation[] = children.filter( + (op) => op.conditionalRender !== ConditionalRender.OnZero, + ); + childrenInstrs = processOperations(onOneOps, registers); + const oneGates: Metadata[] = childrenInstrs.metadataList; + const oneChildWidth: number = childrenInstrs.svgWidth; + + // Subtract startX (left-side) and 2*gatePadding (right-side) from nested child gates width + const width: number = + Math.max(zeroChildWidth, oneChildWidth) - startX - gatePadding * 2; + + metadata.type = GateType.ClassicalControlled; + metadata.children = [zeroGates, oneGates]; + // Add additional width from control button and inner box padding for dashed box + metadata.width = width + controlBtnOffset + groupBoxPadding * 2; + + // Set targets to first and last quantum registers so we can render the surrounding box + // around all quantum registers. + const qubitsY: number[] = Object.values(registers).map(({ y }) => y); + if (qubitsY.length > 0) + metadata.targetsY = [Math.min(...qubitsY), Math.max(...qubitsY)]; + } else if ( + conditionalRender == ConditionalRender.AsGroup && + (children?.length || 0) > 0 + ) { + const childrenInstrs = processOperations( + children as Operation[], + registers, + ); + metadata.type = GateType.Group; + metadata.children = childrenInstrs.metadataList; + // _zoomButton function in gateFormatter.ts relies on + // 'expanded' attribute to render zoom button + metadata.dataAttributes = { expanded: "true" }; + // Subtract startX (left-side) and add inner box padding (minus nested gate padding) for dashed box + metadata.width = + childrenInstrs.svgWidth - startX + (groupBoxPadding - gatePadding) * 2; + } else if (isMeasurement) { + metadata.type = GateType.Measure; + } else if (gate === "SWAP") { + metadata.type = GateType.Swap; + } else if (isControlled) { + metadata.type = gate === "X" ? GateType.Cnot : GateType.ControlledUnitary; + metadata.label = gate; + } else if (gate === "X") { + metadata.type = GateType.X; + metadata.label = gate; + } else { + // Any other gate treated as a simple unitary gate + metadata.type = GateType.Unitary; + metadata.label = gate; + } + + // If adjoint, add ' to the end of gate label + if (isAdjoint && metadata.label.length > 0) metadata.label += "'"; + + // If gate has extra arguments, display them + if (displayArgs != null) metadata.displayArgs = displayArgs; + + // Set gate width + metadata.width = getGateWidth(metadata); + + // Extend existing data attributes with user-provided data attributes + if (dataAttributes != null) + metadata.dataAttributes = { ...metadata.dataAttributes, ...dataAttributes }; + + return metadata; +}; + +/** + * Compute the y coord of a given register. + * + * @param reg Register to compute y coord of. + * @param registers Map of qubit IDs to RegisterMetadata. + * + * @returns The y coord of give register. + */ +const _getRegY = (reg: Register, registers: RegisterMap): number => { + const { type, qId, cId } = reg; + if (!Object.prototype.hasOwnProperty.call(registers, qId)) + throw new Error(`ERROR: Qubit register with ID ${qId} not found.`); + const { y, children } = registers[qId]; + switch (type) { + case undefined: + case RegisterType.Qubit: + return y; + case RegisterType.Classical: + if (children == null) + throw new Error( + `ERROR: No classical registers found for qubit ID ${qId}.`, + ); + if (cId == null) + throw new Error( + `ERROR: No ID defined for classical register associated with qubit ID ${qId}.`, + ); + if (children.length <= cId) + throw new Error( + `ERROR: Classical register ID ${cId} invalid for qubit ID ${qId} with ${children.length} classical register(s).`, + ); + return children[cId].y; + default: + throw new Error(`ERROR: Unknown register type ${type}.`); + } +}; + +/** + * Splits `targets` if non-adjacent or intersected by classical registers. + * + * @param targets Target qubit registers. + * @param classicalRegY y coords of classical registers overlapping current column. + * @param registers Mapping from register qubit IDs to register metadata. + * + * @returns Groups of target qubit y coords. + */ +const _splitTargetsY = ( + targets: Register[], + classicalRegY: number[], + registers: RegisterMap, +): number[][] => { + if (targets.length === 0) return []; + + // Get qIds sorted by ascending y value + const orderedQIds: number[] = Object.keys(registers).map(Number); + orderedQIds.sort((a, b) => registers[a].y - registers[b].y); + const qIdPosition: { [qId: number]: number } = {}; + orderedQIds.forEach((qId, i) => (qIdPosition[qId] = i)); + + // Sort targets and classicalRegY by ascending y value + targets = targets.slice(); + targets.sort((a, b) => { + const posDiff: number = qIdPosition[a.qId] - qIdPosition[b.qId]; + if (posDiff === 0 && a.cId != null && b.cId != null) return a.cId - b.cId; + else return posDiff; + }); + classicalRegY = classicalRegY.slice(); + classicalRegY.sort((a, b) => a - b); + + let prevPos = 0; + let prevY = 0; + + return targets.reduce((groups: number[][], target: Register) => { + const y = _getRegY(target, registers); + const pos = qIdPosition[target.qId]; + + // Split into new group if one of the following holds: + // 1. First target register + // 2. Non-adjacent qubit registers + // 3. There is a classical register between current and previous register + if ( + groups.length === 0 || + pos > prevPos + 1 || + (classicalRegY[0] > prevY && classicalRegY[0] < y) + ) + groups.push([y]); + else groups[groups.length - 1].push(y); + + prevPos = pos; + prevY = y; + + // Remove classical registers that are higher than current y + while (classicalRegY.length > 0 && classicalRegY[0] <= y) + classicalRegY.shift(); + + return groups; + }, []); +}; + +/** + * Updates the x coord of each metadata in the given 2D array of metadata and returns rightmost x coord. + * + * @param opsMetadata 2D array of metadata. + * @param columnWidths Array of column widths. + * + * @returns Rightmost x coord. + */ +const _fillMetadataX = ( + opsMetadata: Metadata[][], + columnWidths: number[], +): number => { + let currX: number = startX; + + const colStartX: number[] = columnWidths.map((width) => { + const x: number = currX; + currX += width + gatePadding * 2; + return x; + }); + + const endX: number = currX; + + opsMetadata.forEach((regOps) => + regOps.forEach((metadata, col) => { + const x = colStartX[col]; + switch (metadata.type) { + case GateType.ClassicalControlled: + case GateType.Group: + { + // Subtract startX offset from nested gates and add offset and padding + let offset: number = x - startX + groupBoxPadding; + if (metadata.type === GateType.ClassicalControlled) + offset += controlBtnOffset; + + // Offset each x coord in children gates + _offsetChildrenX(metadata.children, offset); + + // We don't use the centre x coord because we only care about the rightmost x for + // rendering the box around the group of nested gates + metadata.x = x; + } + break; + + default: + metadata.x = x + columnWidths[col] / 2; + break; + } + }), + ); + + return endX; +}; + +/** + * Offset x coords of nested children operations. + * + * @param children 2D array of children metadata. + * @param offset x coord offset. + */ +const _offsetChildrenX = ( + children: (Metadata | Metadata[])[] | undefined, + offset: number, +): void => { + if (children == null) return; + children.flat().forEach((child) => { + child.x += offset; + _offsetChildrenX(child.children, offset); + }); +}; + +export { + processOperations, + _groupOperations, + _alignOps, + _getClassicalRegStart, + _opToMetadata, + _getRegY, + _splitTargetsY, + _fillMetadataX, + _offsetChildrenX, +}; diff --git a/npm/qsharp/ux/circuit-vis/register.ts b/npm/qsharp/ux/circuit-vis/register.ts new file mode 100644 index 0000000000..ffcd9cd2f8 --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/register.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { + RegisterType, + type Register, + type RegisterMap, + type RegisterMetadata, +} from "../../src/shared/register"; diff --git a/npm/qsharp/ux/circuit-vis/sqore.ts b/npm/qsharp/ux/circuit-vis/sqore.ts new file mode 100644 index 0000000000..2c3263265f --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/sqore.ts @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { formatInputs } from "./formatters/inputFormatter"; +import { formatGates } from "./formatters/gateFormatter"; +import { formatRegisters } from "./formatters/registerFormatter"; +import { processOperations } from "./process"; +import { ConditionalRender, Circuit, Operation } from "./circuit"; +import { Metadata, GateType } from "./metadata"; +import { StyleConfig, style, STYLES } from "./styles"; +import { createUUID } from "./utils"; +import { svgNS } from "./constants"; + +/** + * Contains metadata for visualization. + */ +interface ComposedSqore { + /** Width of visualization. */ + width: number; + /** Height of visualization. */ + height: number; + /** SVG elements the make up the visualization. */ + elements: SVGElement[]; +} + +/** + * Defines the mapping of unique ID to each operation. Used for enabling + * interactivity. + */ +type GateRegistry = { + [id: string]: Operation; +}; + +/** + * Entrypoint class for rendering circuit visualizations. + */ +export class Sqore { + circuit: Circuit; + style: StyleConfig = {}; + gateRegistry: GateRegistry = {}; + + /** + * Initializes Sqore object with custom styles. + * + * @param circuit Circuit to be visualized. + * @param style Custom visualization style. + */ + constructor(circuit: Circuit, style: StyleConfig | string = {}) { + this.circuit = circuit; + this.style = this.getStyle(style); + } + + /** + * Render circuit into `container` at the specified layer depth. + * + * @param container HTML element for rendering visualization into. + * @param renderDepth Initial layer depth at which to render gates. + */ + draw(container: HTMLElement, renderDepth = 0): void { + // Inject into container + if (container == null) throw new Error(`Container not provided.`); + + // Create copy of circuit to prevent mutation + const circuit: Circuit = JSON.parse(JSON.stringify(this.circuit)); + + // Assign unique IDs to each operation + circuit.operations.forEach((op, i) => + this.fillGateRegistry(op, i.toString()), + ); + + // Render operations at starting at given depth + circuit.operations = this.selectOpsAtDepth(circuit.operations, renderDepth); + + // If only one top-level operation, expand automatically: + if ( + circuit.operations.length == 1 && + circuit.operations[0].dataAttributes != null && + // eslint-disable-next-line no-prototype-builtins + circuit.operations[0].dataAttributes.hasOwnProperty("id") + ) { + const id: string = circuit.operations[0].dataAttributes["id"]; + this.expandOperation(circuit.operations, id); + } + + this.renderCircuit(container, circuit); + } + + /** + * Retrieve style for visualization. + * + * @param style Custom style or style name. + * + * @returns Custom style. + */ + private getStyle(style: StyleConfig | string = {}): StyleConfig { + if (typeof style === "string" || style instanceof String) { + const styleName: string = style as string; + // eslint-disable-next-line no-prototype-builtins + if (!STYLES.hasOwnProperty(styleName)) { + console.error(`No style ${styleName} found in STYLES.`); + return {}; + } + style = STYLES[styleName]; + } + return style; + } + + /** + * Render circuit into `container`. + * + * @param container HTML element for rendering visualization into. + * @param circuit Circuit object to be rendered. + */ + private renderCircuit(container: HTMLElement, circuit: Circuit): void { + // Create visualization components + const composedSqore: ComposedSqore = this.compose(circuit); + const svg: SVGElement = this.generateSvg(composedSqore); + container.innerHTML = ""; + container.appendChild(svg); + this.addGateClickHandlers(container, circuit); + } + + /** + * Generates the components required for visualization. + * + * @param circuit Circuit to be visualized. + * + * @returns `ComposedSqore` object containing metadata for visualization. + */ + private compose(circuit: Circuit): ComposedSqore { + const add = (acc: Metadata[], gate: Metadata | Metadata[]): void => { + if (Array.isArray(gate)) { + gate.forEach((g) => add(acc, g)); + } else { + acc.push(gate); + gate.children?.forEach((g) => add(acc, g)); + } + }; + + const flatten = (gates: Metadata[]): Metadata[] => { + const result: Metadata[] = []; + add(result, gates); + return result; + }; + + const { qubits, operations } = circuit; + const { qubitWires, registers, svgHeight } = formatInputs(qubits); + const { metadataList, svgWidth } = processOperations(operations, registers); + const formattedGates: SVGElement = formatGates(metadataList); + const measureGates: Metadata[] = flatten(metadataList).filter( + ({ type }) => type === GateType.Measure, + ); + const formattedRegs: SVGElement = formatRegisters( + registers, + measureGates, + svgWidth, + ); + + const composedSqore: ComposedSqore = { + width: svgWidth, + height: svgHeight, + elements: [qubitWires, formattedRegs, formattedGates], + }; + return composedSqore; + } + + /** + * Generates visualization of `composedSqore` as an SVG. + * + * @param composedSqore ComposedSqore to be visualized. + * + * @returns SVG representation of circuit visualization. + */ + private generateSvg(composedSqore: ComposedSqore): SVGElement { + const { width, height, elements } = composedSqore; + const uuid: string = createUUID(); + + const svg: SVGElement = document.createElementNS(svgNS, "svg"); + svg.setAttribute("id", uuid); + svg.setAttribute("class", "qviz"); + svg.setAttribute("width", width.toString()); + svg.setAttribute("height", height.toString()); + svg.style.setProperty("max-width", "fit-content"); + + // Add styles + const css = document.createElement("style"); + css.innerHTML = style(this.style); + svg.appendChild(css); + + // Add body elements + elements.forEach((element: SVGElement) => svg.appendChild(element)); + + return svg; + } + + /** + * Depth-first traversal to assign unique ID to `operation`. + * The operation is assigned the id `id` and its `i`th child is recursively given + * the id `${id}-${i}`. + * + * @param operation Operation to be assigned. + * @param id: ID to assign to `operation`. + * + */ + private fillGateRegistry(operation: Operation, id: string): void { + if (operation.dataAttributes == null) operation.dataAttributes = {}; + operation.dataAttributes["id"] = id; + // By default, operations cannot be zoomed-out + operation.dataAttributes["zoom-out"] = "false"; + this.gateRegistry[id] = operation; + operation.children?.forEach((childOp, i) => { + this.fillGateRegistry(childOp, `${id}-${i}`); + if (childOp.dataAttributes == null) childOp.dataAttributes = {}; + // Children operations can be zoomed out + childOp.dataAttributes["zoom-out"] = "true"; + }); + // Composite operations can be zoomed in + operation.dataAttributes["zoom-in"] = ( + operation.children != null + ).toString(); + } + + /** + * Pick out operations that are at or below `renderDepth`. + * + * @param operations List of circuit operations. + * @param renderDepth Initial layer depth at which to render gates. + * + * @returns List of operations at or below specifed depth. + */ + private selectOpsAtDepth( + operations: Operation[], + renderDepth: number, + ): Operation[] { + if (renderDepth < 0) + throw new Error( + `Invalid renderDepth of ${renderDepth}. Needs to be >= 0.`, + ); + if (renderDepth === 0) return operations; + return operations + .map((op) => + op.children != null + ? this.selectOpsAtDepth(op.children, renderDepth - 1) + : op, + ) + .flat(); + } + + /** + * Add interactive click handlers to circuit HTML elements. + * + * @param container HTML element containing visualized circuit. + * @param circuit Circuit to be visualized. + * + */ + private addGateClickHandlers(container: HTMLElement, circuit: Circuit): void { + this.addClassicalControlHandlers(container); + this.addZoomHandlers(container, circuit); + } + + /** + * Add interactive click handlers for classically-controlled operations. + * + * @param container HTML element containing visualized circuit. + * + */ + private addClassicalControlHandlers(container: HTMLElement): void { + container.querySelectorAll(".classically-controlled-btn").forEach((btn) => { + // Zoom in on clicked gate + btn.addEventListener("click", (evt: Event) => { + const textSvg = btn.querySelector("text"); + const group = btn.parentElement; + if (textSvg == null || group == null) return; + + const currValue = textSvg.firstChild?.nodeValue; + const zeroGates = group?.querySelector(".gates-zero"); + const oneGates = group?.querySelector(".gates-one"); + switch (currValue) { + case "?": + textSvg.childNodes[0].nodeValue = "1"; + group.classList.remove("classically-controlled-unknown"); + group.classList.remove("classically-controlled-zero"); + group.classList.add("classically-controlled-one"); + zeroGates?.classList.add("hidden"); + oneGates?.classList.remove("hidden"); + break; + case "1": + textSvg.childNodes[0].nodeValue = "0"; + group.classList.remove("classically-controlled-unknown"); + group.classList.add("classically-controlled-zero"); + group.classList.remove("classically-controlled-one"); + zeroGates?.classList.remove("hidden"); + oneGates?.classList.add("hidden"); + break; + case "0": + textSvg.childNodes[0].nodeValue = "?"; + group.classList.add("classically-controlled-unknown"); + group.classList.remove("classically-controlled-zero"); + group.classList.remove("classically-controlled-one"); + zeroGates?.classList.remove("hidden"); + oneGates?.classList.remove("hidden"); + break; + } + evt.stopPropagation(); + }); + }); + } + + /** + * Add interactive click handlers for zoom-in/out functionality. + * + * @param container HTML element containing visualized circuit. + * @param circuit Circuit to be visualized. + * + */ + private addZoomHandlers(container: HTMLElement, circuit: Circuit): void { + container.querySelectorAll(".gate .gate-control").forEach((ctrl) => { + // Zoom in on clicked gate + ctrl.addEventListener("click", (ev: Event) => { + const gateId: string | null | undefined = + ctrl.parentElement?.getAttribute("data-id"); + if (typeof gateId == "string") { + if (ctrl.classList.contains("gate-collapse")) { + this.collapseOperation(circuit.operations, gateId); + } else if (ctrl.classList.contains("gate-expand")) { + this.expandOperation(circuit.operations, gateId); + } + this.renderCircuit(container, circuit); + + ev.stopPropagation(); + } + }); + }); + } + + /** + * Expand selected operation for zoom-in interaction. + * + * @param operations List of circuit operations. + * @param id ID of operation to expand. + * + */ + private expandOperation(operations: Operation[], id: string): void { + operations.forEach((op) => { + if (op.conditionalRender === ConditionalRender.AsGroup) + this.expandOperation(op.children || [], id); + if (op.dataAttributes == null) return op; + const opId: string = op.dataAttributes["id"]; + if (opId === id && op.children != null) { + op.conditionalRender = ConditionalRender.AsGroup; + op.dataAttributes["expanded"] = "true"; + } + }); + } + + /** + * Collapse selected operation for zoom-out interaction. + * + * @param operations List of circuit operations. + * @param id ID of operation to collapse. + * + */ + private collapseOperation(operations: Operation[], parentId: string): void { + operations.forEach((op) => { + if (op.conditionalRender === ConditionalRender.AsGroup) + this.collapseOperation(op.children || [], parentId); + if (op.dataAttributes == null) return op; + const opId: string = op.dataAttributes["id"]; + // Collapse parent gate and its children + if (opId.startsWith(parentId)) { + op.conditionalRender = ConditionalRender.Always; + delete op.dataAttributes["expanded"]; + } + }); + } +} diff --git a/npm/qsharp/ux/circuit-vis/styles.ts b/npm/qsharp/ux/circuit-vis/styles.ts new file mode 100644 index 0000000000..ad22c45b3d --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/styles.ts @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Provides configuration for CSS styles of visualization. + */ +export interface StyleConfig { + /** Line stroke style. */ + lineStroke?: string; + /** Line width. */ + lineWidth?: number; + /** Text colour. */ + textColour?: string; + /** Single qubit unitary fill colour. */ + unitary?: string; + /** Oplus circle fill colour. */ + oplus?: string; + /** Measurement gate fill colour. */ + measure?: string; + /** Measurement unknown primary colour. */ + classicalUnknown?: string; + /** Measurement zero primary colour. */ + classicalZero?: string; + /** Measurement one primary colour. */ + classicalOne?: string; + /** Measurement zero text colour */ + classicalZeroText?: string; + /** Measurement one text colour */ + classicalOneText?: string; +} + +const defaultStyle: StyleConfig = { + lineStroke: "#000000", + lineWidth: 1, + textColour: "#000000", + unitary: "#D9F1FA", + oplus: "#FFFFFF", + measure: "#FFDE86", + classicalUnknown: "#E5E5E5", + classicalZero: "#C40000", + classicalOne: "#4059BD", + classicalZeroText: "#FFFFFF", + classicalOneText: "#FFFFFF", +}; + +const blackAndWhiteStyle: StyleConfig = { + lineStroke: "#000000", + lineWidth: 1, + textColour: "#000000", + unitary: "#FFFFFF", + oplus: "#FFFFFF", + measure: "#FFFFFF", + classicalUnknown: "#FFFFFF", + classicalZero: "#000000", + classicalOne: "#000000", + classicalZeroText: "#FFFFFF", + classicalOneText: "#FFFFFF", +}; + +const invertedStyle: StyleConfig = { + lineStroke: "#FFFFFF", + lineWidth: 1, + textColour: "#FFFFFF", + unitary: "#000000", + oplus: "#000000", + measure: "#000000", + classicalUnknown: "#000000", + classicalZero: "#FFFFFF", + classicalOne: "#FFFFFF", + classicalZeroText: "#000000", + classicalOneText: "#000000", +}; + +/** + * Set of default styles. + */ +export const STYLES: { [name: string]: StyleConfig } = { + /** Default style with coloured gates. */ + Default: defaultStyle, + /** Black and white style. */ + BlackAndWhite: blackAndWhiteStyle, + /** Inverted black and white style (for black backgrounds). */ + Inverted: invertedStyle, +}; + +/** + * CSS style script to be injected into visualization SVG. + * + * @param customStyle Custom style configuration. + * + * @returns String containing CSS style script. + */ +export const style = (customStyle: StyleConfig = {}): string => { + const styleConfig = { ...defaultStyle, ...customStyle }; + + return `${_defaultGates(styleConfig)} + ${_classicallyControlledGates(styleConfig)} + ${_expandCollapse}`; +}; + +const _defaultGates = (styleConfig: StyleConfig): string => ` + line, + circle, + rect { + stroke: ${styleConfig.lineStroke}; + stroke-width: ${styleConfig.lineWidth}; + } + text { + fill: ${styleConfig.textColour}; + dominant-baseline: middle; + text-anchor: middle; + font-family: Arial; + } + .control-dot { + fill: ${styleConfig.lineStroke}; + } + .oplus line, .oplus circle { + fill: ${styleConfig.oplus}; + stroke-width: 2; + } + .gate-unitary { + fill: ${styleConfig.unitary}; + } + .gate-measure { + fill: ${styleConfig.measure}; + } + rect.gate-swap { + fill: transparent; + stroke: transparent; + } + .arc-measure { + stroke: ${styleConfig.lineStroke}; + fill: none; + stroke-width: ${styleConfig.lineWidth}; + } + .register-classical { + stroke-width: ${(styleConfig.lineWidth || 0) / 2}; + }`; + +const _classicallyControlledGates = (styleConfig: StyleConfig): string => { + const gateOutline = ` + .classically-controlled-one .classical-container, + .classically-controlled-one .classical-line { + stroke: ${styleConfig.classicalOne}; + stroke-width: ${(styleConfig.lineWidth || 0) + 0.3}; + fill: ${styleConfig.classicalOne}; + fill-opacity: 0.1; + } + .classically-controlled-zero .classical-container, + .classically-controlled-zero .classical-line { + stroke: ${styleConfig.classicalZero}; + stroke-width: ${(styleConfig.lineWidth || 0) + 0.3}; + fill: ${styleConfig.classicalZero}; + fill-opacity: 0.1; + }`; + const controlBtn = ` + .classically-controlled-btn { + cursor: pointer; + } + .classically-controlled-unknown .classically-controlled-btn { + fill: ${styleConfig.classicalUnknown}; + } + .classically-controlled-one .classically-controlled-btn { + fill: ${styleConfig.classicalOne}; + } + .classically-controlled-zero .classically-controlled-btn { + fill: ${styleConfig.classicalZero}; + }`; + + const controlBtnText = ` + .classically-controlled-btn text { + dominant-baseline: middle; + text-anchor: middle; + stroke: none; + font-family: Arial; + } + .classically-controlled-unknown .classically-controlled-btn text { + fill: ${styleConfig.textColour}; + } + .classically-controlled-one .classically-controlled-btn text { + fill: ${styleConfig.classicalOneText}; + } + .classically-controlled-zero .classically-controlled-btn text { + fill: ${styleConfig.classicalZeroText}; + }`; + + return ` + .hidden { + display: none; + } + .classically-controlled-unknown { + opacity: 0.25; + } + + ${gateOutline} + ${controlBtn} + ${controlBtnText}`; +}; + +const _expandCollapse = ` + .qviz .gate-collapse, + .qviz .gate-expand { + opacity: 0; + transition: opacity 1s; + } + + .qviz:hover .gate-collapse, + .qviz:hover .gate-expand { + visibility: visible; + opacity: 0.2; + transition: visibility 1s; + transition: opacity 1s; + } + + .gate-expand, .gate-collapse { + cursor: pointer; + } + + .gate-collapse circle, + .gate-expand circle { + fill: white; + stroke-width: 2px; + stroke: black; + } + .gate-collapse path, + .gate-expand path { + stroke-width: 4px; + stroke: black; + } + + .gate:hover > .gate-collapse, + .gate:hover > .gate-expand { + visibility: visible; + opacity: 1; + transition: opacity 1s; + }`; diff --git a/npm/qsharp/ux/circuit-vis/utils.ts b/npm/qsharp/ux/circuit-vis/utils.ts new file mode 100644 index 0000000000..8ce9f9d4eb --- /dev/null +++ b/npm/qsharp/ux/circuit-vis/utils.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Metadata, GateType } from "./metadata"; +import { + minGateWidth, + labelPadding, + labelFontSize, + argsFontSize, +} from "./constants"; + +/** + * Generate a UUID using `Math.random`. + * Note: this implementation came from https://stackoverflow.com/questions/105034/how-to-create-guid-uuid + * and is not cryptographically secure but works for our use case. + * + * @returns UUID string. + */ +const createUUID = (): string => + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + +/** + * Calculate the width of a gate, given its metadata. + * + * @param metadata Metadata of a given gate. + * + * @returns Width of given gate (in pixels). + */ +const getGateWidth = ({ + type, + label, + displayArgs, + width, +}: Metadata): number => { + if (width > 0) return width; + + switch (type) { + case GateType.Measure: + case GateType.Cnot: + case GateType.Swap: + return minGateWidth; + default: { + const labelWidth = _getStringWidth(label); + const argsWidth = + displayArgs != null ? _getStringWidth(displayArgs, argsFontSize) : 0; + const textWidth = Math.max(labelWidth, argsWidth) + labelPadding * 2; + return Math.max(minGateWidth, textWidth); + } + } +}; + +/** + * Get the width of a string with font-size `fontSize` and font-family Arial. + * + * @param text Input string. + * @param fontSize Font size of `text`. + * + * @returns Pixel width of given string. + */ +const _getStringWidth = ( + text: string, + fontSize: number = labelFontSize, +): number => { + const canvas: HTMLCanvasElement = document.createElement("canvas"); + const context: CanvasRenderingContext2D | null = canvas.getContext("2d"); + if (context == null) throw new Error("Null canvas"); + + context.font = `${fontSize}px Arial`; + const metrics: TextMetrics = context.measureText(text); + return metrics.width; +}; + +export { createUUID, getGateWidth, _getStringWidth }; diff --git a/npm/qsharp/ux/circuit.tsx b/npm/qsharp/ux/circuit.tsx index 6b5e0861f4..a4bab823d1 100644 --- a/npm/qsharp/ux/circuit.tsx +++ b/npm/qsharp/ux/circuit.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as qviz from "@microsoft/quantum-viz.js/lib"; +import * as qviz from "./circuit-vis"; import { useEffect, useRef, useState } from "preact/hooks"; import { CircuitProps } from "./data.js"; import { Spinner } from "./spinner.js"; @@ -131,7 +131,7 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) { function renderCircuit(circuit: qviz.Circuit, container: HTMLDivElement) { qviz.draw(circuit, container); - // quantum-viz hardcodes the styles in the SVG. + // circuit-vis hardcodes the styles in the SVG. // Remove the style elements -- we'll define the styles in our own CSS. const styleElements = container.querySelectorAll("style"); styleElements?.forEach((tag) => tag.remove()); diff --git a/npm/qsharp/ux/data.ts b/npm/qsharp/ux/data.ts index 72a3ab4be4..8f4c9f33a5 100644 --- a/npm/qsharp/ux/data.ts +++ b/npm/qsharp/ux/data.ts @@ -75,4 +75,4 @@ export type CircuitProps = { calculating: boolean; }; -export type CircuitData = import("@microsoft/quantum-viz.js/lib").Circuit; +export type CircuitData = import("../src/shared/circuit").Circuit; diff --git a/package-lock.json b/package-lock.json index 3a42417cb6..0587373d98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "devDependencies": { "@anywidget/types": "^0.1.4", "@eslint/js": "^9.9.1", - "@microsoft/quantum-viz.js": "^1.0.5", "@parcel/watcher": "^2.4.1", "@types/chai": "^4.3.8", "@types/markdown-it": "^14.1.1", @@ -1451,12 +1450,6 @@ "integrity": "sha512-gNw9z9LbqLV+WadZ6/MMrWwO3e0LuoUH1wve/1iPsBNbgqeVCiB0EZFNNj2lysxS2gkqoF9hmyVaG3MoM1BkxA==", "dev": true }, - "node_modules/@microsoft/quantum-viz.js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@microsoft/quantum-viz.js/-/quantum-viz.js-1.0.5.tgz", - "integrity": "sha512-U2atugL4j+p/yiioPR/TfqqR534iP6kEa10S+YNUW68UyvMe8eXNDFlb8GG4Ro9WSGobyLa8tP7iRiR2YMZO1w==", - "dev": true - }, "node_modules/@nevware21/ts-async": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.0.tgz", diff --git a/package.json b/package.json index ee66c0d6f1..86ecd521f5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@anywidget/types": "^0.1.4", "@eslint/js": "^9.9.1", - "@microsoft/quantum-viz.js": "^1.0.5", "@parcel/watcher": "^2.4.1", "@types/chai": "^4.3.8", "@types/markdown-it": "^14.1.1", diff --git a/vscode/src/circuit.ts b/vscode/src/circuit.ts index c26e3250be..159cf99813 100644 --- a/vscode/src/circuit.ts +++ b/vscode/src/circuit.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { type Circuit as CircuitData } from "@microsoft/quantum-viz.js/lib"; import { escapeHtml } from "markdown-it/lib/common/utils.mjs"; import { + type CircuitData, ICompilerWorker, IOperationInfo, IQSharpError,