From ca1ca3c2e98198f9398c93e1a52a1e5785f50819 Mon Sep 17 00:00:00 2001 From: Raphael Koh Date: Thu, 9 Jul 2020 22:09:02 -0400 Subject: [PATCH 1/4] Add path visualizer JS files --- .../ExecutionPathVisualizer/constants.ts | 51 ++ .../ExecutionPathVisualizer/executionPath.ts | 43 ++ .../ExecutionPathVisualizer/fontSizes.ts | 587 ++++++++++++++++++ .../formatters/formatUtils.ts | 104 ++++ .../formatters/gateFormatter.ts | 308 +++++++++ .../formatters/inputFormatter.ts | 68 ++ .../formatters/registerFormatter.ts | 77 +++ .../client/ExecutionPathVisualizer/index.ts | 193 ++++++ .../ExecutionPathVisualizer/metadata.ts | 29 + .../ExecutionPathVisualizer/pathVisualizer.ts | 121 ++++ .../client/ExecutionPathVisualizer/process.ts | 348 +++++++++++ .../ExecutionPathVisualizer/register.ts | 32 + .../client/ExecutionPathVisualizer/utils.ts | 53 ++ src/Kernel/client/kernel.ts | 11 +- 14 files changed, 2020 insertions(+), 5 deletions(-) create mode 100644 src/Kernel/client/ExecutionPathVisualizer/constants.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/executionPath.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/formatters/inputFormatter.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/formatters/registerFormatter.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/index.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/metadata.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/process.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/register.ts create mode 100644 src/Kernel/client/ExecutionPathVisualizer/utils.ts diff --git a/src/Kernel/client/ExecutionPathVisualizer/constants.ts b/src/Kernel/client/ExecutionPathVisualizer/constants.ts new file mode 100644 index 0000000000..aea7c9deb6 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/constants.ts @@ -0,0 +1,51 @@ +/** + * Enum for the various gate operations handled. + */ +export enum GateType { + /** Measurement gate. */ + Measure, + /** CNOT gate. */ + Cnot, + /** SWAP gate. */ + Swap, + /** Single/multi qubit unitary gate. */ + Unitary, + /** Single/multi controlled unitary gate. */ + ControlledUnitary, + /** Nested group of classically-controlled gates. */ + ClassicalControlled, + /** Invalid gate. */ + Invalid +}; + +// Display attributes +/** Left padding of SVG. */ +export const leftPadding: number = 20; +/** x coordinate for first operation on each register. */ +export const startX: number = 80; +/** y coordinate of first register. */ +export const startY: number = 40; +/** Minimum width of each gate. */ +export const minGateWidth: number = 40; +/** Height of each gate. */ +export const gateHeight: number = 40; +/** Padding on each side of gate. */ +export const gatePadding: number = 10; +/** Padding on each side of gate label. */ +export const labelPadding: number = 10; +/** Height between each qubit register. */ +export const registerHeight: number = gateHeight + gatePadding * 2; +/** Height between classical registers. */ +export const classicalRegHeight: number = gateHeight; +/** Classical box inner padding. */ +export const classicalBoxPadding: number = 15; +/** Additional offset for control button. */ +export const controlBtnOffset: number = 40; +/** Control button radius. */ +export const controlBtnRadius: number = 15; +/** Default font size for gate labels. */ +export const labelFontSize: number = 14; +/** Default font size for gate arguments. */ +export const argsFontSize: number = 12; +/** Starting x coord for each register wire. */ +export const regLineStart: number = 40; diff --git a/src/Kernel/client/ExecutionPathVisualizer/executionPath.ts b/src/Kernel/client/ExecutionPathVisualizer/executionPath.ts new file mode 100644 index 0000000000..314230fb84 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/executionPath.ts @@ -0,0 +1,43 @@ +import { Register } from "./register"; + +/** + * Structure of JSON representation of the execution path of a Q# operation. + */ +export interface ExecutionPath { + /** 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; +}; + +/** + * Represents an operation and the registers it acts on. + */ +export interface Operation { + /** Gate label. */ + gate: string; + /** Gate arguments as string. */ + argStr?: string, + /** Classically-controlled gates. + * - children[0]: gates when classical control bit is 0. + * - children[1]: gates when classical control bit is 1. + */ + children?: Operation[][]; + /** Whether gate is a controlled operation. */ + controlled: boolean; + /** Whether gate is an adjoint operation. */ + adjoint: boolean; + /** Control registers the gate acts on. */ + controls: Register[]; + /** Target registers the gate acts on. */ + targets: Register[]; +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts b/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts new file mode 100644 index 0000000000..e824a064f9 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts @@ -0,0 +1,587 @@ +// Retrieved from https://github.com/adambisek/string-pixel-width/blob/master/src/widthsMap.js + +/** + * Provides a mapping from character to an array of font sizes represented by: + * [normal, bold, italic, bold + italic] + */ +interface FontSizeMap { + [ch: string]: [number, number, number, number]; +}; + +/** + * Pixel width of each character with font-family Arial. + */ +const fontSizes: FontSizeMap = { + "0": [ + 56, + 56, + 56, + 56 + ], + "1": [ + 56, + 56, + 56, + 56 + ], + "2": [ + 56, + 56, + 56, + 56 + ], + "3": [ + 56, + 56, + 56, + 56 + ], + "4": [ + 56, + 56, + 56, + 56 + ], + "5": [ + 56, + 56, + 56, + 56 + ], + "6": [ + 56, + 56, + 56, + 56 + ], + "7": [ + 56, + 56, + 56, + 56 + ], + "8": [ + 56, + 56, + 56, + 56 + ], + "9": [ + 56, + 56, + 56, + 56 + ], + " ": [ + 28, + 28, + 28, + 28 + ], + "!": [ + 28, + 33, + 28, + 33 + ], + "\"": [ + 35, + 47, + 35, + 47 + ], + "#": [ + 56, + 56, + 56, + 56 + ], + "$": [ + 56, + 56, + 56, + 56 + ], + "%": [ + 89, + 89, + 89, + 89 + ], + "&": [ + 67, + 72, + 67, + 72 + ], + "'": [ + 19, + 24, + 19, + 24 + ], + "(": [ + 33, + 33, + 33, + 33 + ], + ")": [ + 33, + 33, + 33, + 33 + ], + "*": [ + 39, + 39, + 39, + 39 + ], + "+": [ + 58, + 58, + 58, + 58 + ], + ",": [ + 28, + 28, + 28, + 28 + ], + "-": [ + 33, + 33, + 33, + 33 + ], + ".": [ + 28, + 28, + 28, + 28 + ], + "/": [ + 28, + 28, + 28, + 28 + ], + ":": [ + 28, + 33, + 28, + 33 + ], + ";": [ + 28, + 33, + 28, + 33 + ], + "<": [ + 58, + 58, + 58, + 58 + ], + "=": [ + 58, + 58, + 58, + 58 + ], + ">": [ + 58, + 58, + 58, + 58 + ], + "?": [ + 56, + 61, + 56, + 61 + ], + "@": [ + 102, + 98, + 102, + 98 + ], + "A": [ + 67, + 72, + 67, + 72 + ], + "B": [ + 67, + 72, + 67, + 72 + ], + "C": [ + 72, + 72, + 72, + 72 + ], + "D": [ + 72, + 72, + 72, + 72 + ], + "E": [ + 67, + 67, + 67, + 67 + ], + "F": [ + 61, + 61, + 61, + 61 + ], + "G": [ + 78, + 78, + 78, + 78 + ], + "H": [ + 72, + 72, + 72, + 72 + ], + "I": [ + 28, + 28, + 28, + 28 + ], + "J": [ + 50, + 56, + 50, + 56 + ], + "K": [ + 67, + 72, + 67, + 72 + ], + "L": [ + 56, + 61, + 56, + 61 + ], + "M": [ + 83, + 83, + 83, + 83 + ], + "N": [ + 72, + 72, + 72, + 72 + ], + "O": [ + 78, + 78, + 78, + 78 + ], + "P": [ + 67, + 67, + 67, + 67 + ], + "Q": [ + 78, + 78, + 78, + 78 + ], + "R": [ + 72, + 72, + 72, + 72 + ], + "S": [ + 67, + 67, + 67, + 67 + ], + "T": [ + 61, + 61, + 61, + 61 + ], + "U": [ + 72, + 72, + 72, + 72 + ], + "V": [ + 67, + 67, + 67, + 67 + ], + "W": [ + 94, + 94, + 94, + 94 + ], + "X": [ + 67, + 67, + 67, + 67 + ], + "Y": [ + 67, + 67, + 67, + 67 + ], + "Z": [ + 61, + 61, + 61, + 61 + ], + "[": [ + 28, + 33, + 28, + 33 + ], + "\\": [ + 28, + 28, + 28, + 28 + ], + "]": [ + 28, + 33, + 28, + 33 + ], + "^": [ + 47, + 58, + 47, + 58 + ], + "_": [ + 56, + 56, + 56, + 56 + ], + "`": [ + 33, + 33, + 33, + 33 + ], + "a": [ + 56, + 56, + 56, + 56 + ], + "b": [ + 56, + 61, + 56, + 61 + ], + "c": [ + 50, + 56, + 50, + 56 + ], + "d": [ + 56, + 61, + 56, + 61 + ], + "e": [ + 56, + 56, + 56, + 56 + ], + "f": [ + 28, + 33, + 28, + 33 + ], + "g": [ + 56, + 61, + 56, + 61 + ], + "h": [ + 56, + 61, + 56, + 61 + ], + "i": [ + 22, + 28, + 22, + 28 + ], + "j": [ + 22, + 28, + 22, + 28 + ], + "k": [ + 50, + 56, + 50, + 56 + ], + "l": [ + 22, + 28, + 22, + 28 + ], + "m": [ + 83, + 89, + 83, + 89 + ], + "n": [ + 56, + 61, + 56, + 61 + ], + "o": [ + 56, + 61, + 56, + 61 + ], + "p": [ + 56, + 61, + 56, + 61 + ], + "q": [ + 56, + 61, + 56, + 61 + ], + "r": [ + 33, + 39, + 33, + 39 + ], + "s": [ + 50, + 56, + 50, + 56 + ], + "t": [ + 28, + 33, + 28, + 33 + ], + "u": [ + 56, + 61, + 56, + 61 + ], + "v": [ + 50, + 56, + 50, + 56 + ], + "w": [ + 72, + 78, + 72, + 78 + ], + "x": [ + 50, + 56, + 50, + 56 + ], + "y": [ + 50, + 56, + 50, + 56 + ], + "z": [ + 50, + 50, + 50, + 50 + ], + "{": [ + 33, + 39, + 33, + 39 + ], + "|": [ + 26, + 28, + 26, + 28 + ], + "}": [ + 33, + 39, + 33, + 39 + ], + "~": [ + 58, + 58, + 58, + 58 + ] +}; + +export default fontSizes; diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts new file mode 100644 index 0000000000..dbceb2275e --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts @@ -0,0 +1,104 @@ +import { labelFontSize } from "../constants"; + +// Helper functions for basic SVG components + +/** + * Given an array of SVG elements, group them as an SVG group using the `` tag. + * + * @param svgElems Array of SVG elements. + * + * @returns SVG string for grouped elements. + */ +export const group = (...svgElems: (string | string[])[]): string => + ['', ...svgElems.flat(), ''].join('\n'); + +/** + * 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 string for control dot. + */ +export const controlDot = (x: number, y: number, radius: number = 5): string => + ``; + +/** + * 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 strokeWidth Stroke width of line. + * + * @returns SVG string for line. + */ +export const line = (x1: number, y1: number, x2: number, y2: number, strokeWidth: number = 1): string => + ``; + +/** + * 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. + * + * @returns SVG string for unitary box. + */ +export const box = (x: number, y: number, width: number, height: number): string => + ``; + +/** + * 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 string for text. + */ +export const text = (text: string, x: number, y: number, fs: number = labelFontSize): string => + `${text}`; + +/** + * 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 string for arc. + */ +export const arc = (x: number, y: number, rx: number, ry: number): string => + ``; + +/** + * 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. + * + * @returns SVG string for dashed line. + */ +export const dashedLine = (x1: number, y1: number, x2: number, y2: number): string => + ``; + +/** + * 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. + * + * @returns SVG string for dashed box. + */ +export const dashedBox = (x: number, y: number, width: number, height: number): string => + ``; diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts new file mode 100644 index 0000000000..4d57440722 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts @@ -0,0 +1,308 @@ +import { Metadata } from "../metadata"; +import { + GateType, + minGateWidth, + gateHeight, + registerHeight, + labelFontSize, + argsFontSize, + controlBtnRadius, + controlBtnOffset, + classicalBoxPadding, + classicalRegHeight, +} from "../constants"; +import { + group, + controlDot, + line, + 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. + * + * @returns SVG representation of operations. + */ +const formatGates = (opsMetadata: Metadata[]): string => { + const formattedGates: string[] = opsMetadata.map(_formatGate); + return formattedGates.flat().join('\n'); +}; + +/** + * Takes in an operation's metadata and formats it into SVG. + * + * @param metadata Metadata object representation of gate. + * + * @returns SVG representation of gate. + */ +const _formatGate = (metadata: Metadata): string => { + const { type, x, controlsY, targetsY, label, argStr, width } = metadata; + switch (type) { + case GateType.Measure: + return _measure(x, controlsY[0], targetsY[0]); + case GateType.Unitary: + return _unitary(label, x, targetsY, width, argStr); + case GateType.Swap: + if (controlsY.length > 0) return _controlledGate(metadata); + else return _swap(x, targetsY); + case GateType.Cnot: + case GateType.ControlledUnitary: + return _controlledGate(metadata); + case GateType.ClassicalControlled: + return _classicalControlled(metadata); + default: + throw new Error(`ERROR: unknown gate (${label}) of type ${type}.`); + } +}; + +/** + * Creates a measurement gate at the x position, where qy and cy are + * the y coords of the qubit register and classical register, respectively. + * + * @param x x coord of measurement gate. + * @param qy y coord of qubit register. + * @param cy y coord of classical register. + * + * @returns SVG representation of measurement gate. + */ +const _measure = (x: number, qy: number, cy: number): string => { + x -= minGateWidth / 2; + const width: number = minGateWidth, height = gateHeight; + // Draw measurement box + const mBox: string = box(x, qy - height / 2, width, height); + const mArc: string = arc(x + 5, qy + 2, width / 2 - 5, height / 2 - 8); + const meter: string = line(x + width / 2, qy + 8, x + width - 8, qy - height / 2 + 8); + const svg: string = group(mBox, mArc, meter); + return svg; +}; + +/** + * 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 argStr 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, argStr?: string, renderDashedLine: boolean = true): string => { + if (y.length === 0) return ""; + + // Sort y in ascending order + y.sort((y1, y2) => y1 - y2); + + // Group adjacent registers + let prevY: number = y[0]; + const regGroups: number[][] = y.reduce((acc: number[][], currY: number) => { + // Registers are defined to be adjacent if they differ by registerHeight in their y coord + // Raphael: Is there a better way of doing this? (i.e. split into y and heights in processOperations) + if (acc.length === 0 || currY - prevY > registerHeight) acc.push([currY]); + else acc[acc.length - 1].push(currY); + prevY = currY; + return acc; + }, []); + + // Render each group as a separate unitary boxes + const unitaryBoxes: string[] = regGroups.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, argStr); + }); + + // Draw dashed line between disconnected unitaries + if (renderDashedLine && unitaryBoxes.length > 1) { + const maxY: number = y[y.length - 1], minY: number = y[0]; + const vertLine: string = dashedLine(x, minY, x, maxY); + return [vertLine, ...unitaryBoxes].join('\n'); + } else return unitaryBoxes.join('\n'); +}; + +/** + * 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 argStr Arguments passed in to gate. + * + * @returns SVG representation of unitary box. + */ +const _unitaryBox = (label: string, x: number, y: number, width: number, height: number = gateHeight, argStr?: string): string => { + y -= gateHeight / 2; + const uBox: string = box(x - width / 2, y, width, height); + const labelY = y + height / 2 - ((argStr == null) ? 0 : 7); + const labelText: string = text(label, x, labelY); + const elems = [uBox, labelText]; + if (argStr != null) { + const argStrY = y + height / 2 + 8; + const argText: string = text(argStr, x, argStrY, argsFontSize); + elems.push(argText); + } + const svg: string = group(elems); + return svg; +}; + +/** + * 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 = (x: number, targetsY: number[]): string => { + // Get SVGs of crosses + const crosses: string[] = targetsY.map(y => _cross(x, y)); + const vertLine: string = line(x, targetsY[0], x, targetsY[1]); + const svg: string = group(crosses, vertLine); + return svg; +}; + +/** + * 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): string => { + const radius: number = 8; + const line1: string = line(x - radius, y - radius, x + radius, y + radius); + const line2: string = line(x - radius, y + radius, x + radius, y - radius); + return [line1, line2].join('\n'); +}; + +/** + * 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): string => { + const targetGateSvgs: string[] = []; + const { type, x, controlsY, targetsY, label, argStr, width } = metadata; + // Get SVG for target gates + switch (type) { + case GateType.Cnot: + targetsY.forEach(y => targetGateSvgs.push(_oplus(x, y))); + break; + case GateType.Swap: + targetsY.forEach(y => targetGateSvgs.push(_cross(x, y))); + break; + case GateType.ControlledUnitary: + targetGateSvgs.push(_unitary(label, x, targetsY, width, argStr, false)); + break; + default: + throw new Error(`ERROR: Unrecognized gate: ${label} of type ${type}`); + } + // Get SVGs for control dots + const controlledDotsSvg: string[] = controlsY.map(y => controlDot(x, y)); + // Create control lines + const maxY: number = Math.max(...controlsY, ...targetsY); + const minY: number = Math.min(...controlsY, ...targetsY); + const vertLine: string = line(x, minY, x, maxY); + const svg: string = group(vertLine, controlledDotsSvg, targetGateSvgs); + 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: number = 15): string => { + const circle: string = ``; + const vertLine: string = line(x, y - r, x, y + r); + const horLine: string = line(x - r, y, x + r, y); + const svg: string = group(circle, vertLine, horLine); + return svg; +} + +/** + * Generates the SVG for a classically controlled group of oeprations. + * + * @param metadata Metadata representation of gate. + * @param padding Padding within dashed box. + * + * @returns SVG representation of gate. + */ +const _classicalControlled = (metadata: Metadata, padding: number = classicalBoxPadding): string => { + let { x, controlsY, targetsY, width, children, htmlClass } = metadata; + + const controlY = controlsY[0]; + if (htmlClass == null) htmlClass = 'cls-control'; + + // Get SVG for gates controlled on 0 and make them hidden initially + let childrenZero: string = (children != null) ? formatGates(children[0]) : ''; + childrenZero = ``; + + // Get SVG for gates controlled on 1 + let childrenOne: string = (children != null) ? formatGates(children[1]) : ''; + childrenOne = `\r\n${childrenOne}`; + + // Draw control button and attached dashed line to dashed box + const controlCircleX: number = x + controlBtnRadius; + const controlCircle: string = _controlCircle(controlCircleX, controlY, htmlClass); + const lineY1: number = controlY + controlBtnRadius, lineY2: number = controlY + classicalRegHeight / 2; + const vertLine: string = dashedLine(controlCircleX, lineY1, controlCircleX, lineY2); + x += controlBtnOffset; + const horLine: string = dashedLine(controlCircleX, lineY2, x, lineY2); + + width = width - controlBtnOffset + (padding - classicalBoxPadding) * 2; + x += classicalBoxPadding - 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: string = dashedBox(x, y, width, height); + + // Display controlled operation in initial "unknown" state + const svg: string = group(``, horLine, vertLine, + controlCircle, childrenZero, childrenOne, box, ''); + + return svg; +}; + +/** + * 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 cls Class name. + * @param r Radius of circle. + * + * @returns SVG representation of control circle. + */ +const _controlCircle = (x: number, y: number, cls: string, r: number = controlBtnRadius): string => + ` + +? +`; + +export { + formatGates, + _formatGate, + _measure, + _unitary, + _swap, + _controlledGate, + _classicalControlled, +}; \ No newline at end of file diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/inputFormatter.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/inputFormatter.ts new file mode 100644 index 0000000000..9a24ed6698 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/inputFormatter.ts @@ -0,0 +1,68 @@ +import { Qubit } from "../executionPath"; +import { RegisterType, RegisterMap, RegisterMetadata } from "../register"; +import { + leftPadding, + startY, + registerHeight, + classicalRegHeight, +} from "../constants"; + +/** + * `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: string, registers: RegisterMap, svgHeight: number } => { + const qubitWires: string[] = []; + 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: qubitWires.join('\n'), + 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): string => + `|0⟩`; + +export { + formatInputs, + _qubitInput, +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/registerFormatter.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/registerFormatter.ts new file mode 100644 index 0000000000..3a8447005a --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/registerFormatter.ts @@ -0,0 +1,77 @@ +import { RegisterMap } from "../register"; +import { regLineStart, GateType } from "../constants"; +import { Metadata } from "../metadata"; +import { line } 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): string => { + const formattedRegs: string[] = []; + // 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.forEach(y => { + formattedRegs.push(_classicalRegister(x, gateY, endX, y)); + }); + }); + return formattedRegs.join('\n'); +}; + +/** + * 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): string => { + const wirePadding: number = 1; + // Draw vertical lines + const vLine1: string = line(startX + wirePadding, gateY, startX + wirePadding, wireY - wirePadding, 0.5); + const vLine2: string = line(startX - wirePadding, gateY, startX - wirePadding, wireY + wirePadding, 0.5); + // Draw horizontal lines + const hLine1: string = line(startX + wirePadding, wireY - wirePadding, endX, wireY - wirePadding, 0.5); + const hLine2: string = line(startX - wirePadding, wireY + wirePadding, endX, wireY + wirePadding, 0.5); + const svg: string = [vLine1, vLine2, hLine1, hLine2].join('\n'); + return svg; +}; + +/** + * 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: number = 16): string => { + const labelY: number = y - labelOffset; + const wire: string = line(regLineStart, y, endX, y); + const label: string = `q${qId}`; + const svg: string = [wire, label].join('\n'); + return svg; +}; + +export { + formatRegisters, + _classicalRegister, + _qubitRegister, +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/index.ts b/src/Kernel/client/ExecutionPathVisualizer/index.ts new file mode 100644 index 0000000000..9b94bff735 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/index.ts @@ -0,0 +1,193 @@ +import * as fs from "fs"; +import * as path from "path"; +import { ExecutionPath } from "./executionPath"; +import { jsonToSvg, jsonToHtml } from "./pathVisualizer"; + +const exampleJSON: ExecutionPath = { + qubits: [ + { id: 0, numChildren: 1 }, + { id: 1 }, + { id: 2 }, + { id: 3 } + ], + operations: [ + { + "gate": "H", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [{ type: 0, qId: 1 }] + }, + { + "gate": "RX", + "argStr": "(0.25)", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 1 }], + "targets": [{ type: 0, qId: 0 }] + }, + { + "gate": "X", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 1 }], + "targets": [ + { type: 0, qId: 2 }, + { type: 0, qId: 3 } + ] + }, + { + "gate": "X", + "controlled": true, + "adjoint": false, + "controls": [ + { type: 0, qId: 2 }, + { type: 0, qId: 3 } + ], + "targets": [ + { type: 0, qId: 1 } + ] + }, + { + "gate": "X", + "controlled": true, + "adjoint": false, + "controls": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ], + "targets": [{ type: 0, qId: 2 }] + }, + { + "gate": "X", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 2 }], + "targets": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ] + }, + { + "gate": "measure", + "controlled": false, + "adjoint": false, + "controls": [{ type: 0, qId: 0 }], + "targets": [{ type: 1, qId: 0, cId: 0 }] + }, + { + "gate": "if", + "controlled": false, + "adjoint": false, + "controls": [{ type: 1, qId: 0, cId: 0 }], + "targets": [], + children: [[ + { + "gate": "H", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [{ type: 0, qId: 1 }] + }, + { + "gate": "X", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [{ type: 0, qId: 1 }] + } + ], + [ + { + "gate": "X", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 0 }], + "targets": [{ type: 0, qId: 1 }] + }, + { + "gate": "Foo", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [{ type: 0, qId: 3 }] + } + ]] + }, + { + "gate": "SWAP", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [ + { type: 0, qId: 0 }, + { type: 0, qId: 2 } + ] + }, + { + "gate": "ZZ", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ] + }, + { + "gate": "ZZ", + "controlled": false, + "adjoint": false, + "controls": [], + "targets": [ + { type: 0, qId: 0 }, + { type: 0, qId: 1 } + ] + }, + { + "gate": "XX", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 0 }], + "targets": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ] + }, + { + "gate": "XX", + "controlled": true, + "adjoint": false, + "controls": [{ type: 0, qId: 2 }], + "targets": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ] + }, + { + "gate": "XX", + "controlled": true, + "adjoint": false, + "controls": [ + { type: 0, qId: 0 }, + { type: 0, qId: 2 } + ], + "targets": [ + { type: 0, qId: 1 }, + { type: 0, qId: 3 } + ] + }, + ] +}; + +const saveToFile = (fileName: string, contents: string): void => { + const filePath: string = path.join(__dirname, fileName); + fs.writeFile(filePath, contents, 'utf8', (err) => console.log(err)); + +}; + +// Example use case () +const svg = jsonToSvg(exampleJSON); +saveToFile('../example.svg', svg); +const html = jsonToHtml(exampleJSON); +saveToFile('../example.html', html); diff --git a/src/Kernel/client/ExecutionPathVisualizer/metadata.ts b/src/Kernel/client/ExecutionPathVisualizer/metadata.ts new file mode 100644 index 0000000000..e436d89722 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/metadata.ts @@ -0,0 +1,29 @@ +import { GateType } from "./constants"; + +/** + * 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. */ + targetsY: number[]; + /** Gate label. */ + label: string; + /** Gate arguments as string. */ + argStr?: string, + /** Gate width. */ + width: number; + /** Classically-controlled gates. + * - children[0]: gates when classical control bit is 0. + * - children[1]: gates when classical control bit is 1. + */ + children?: Metadata[][]; + /** HTML element class for interactivity. */ + htmlClass?: string; +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts new file mode 100644 index 0000000000..f43bd6c3af --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts @@ -0,0 +1,121 @@ +import { formatInputs } from "./formatters/inputFormatter"; +import { formatGates } from "./formatters/gateFormatter"; +import { formatRegisters } from "./formatters/registerFormatter"; +import { processOperations } from "./process"; +import { ExecutionPath } from "./executionPath"; +import { Metadata } from "./metadata"; +import { GateType } from "./constants"; + +const script = ` + +`; + +const style = ` + +`; + +/** + * Converts JSON representing an execution path of a Q# program given by the simulator and returns its HTML visualization. + * + * @param json JSON received from simulator. + * + * @returns HTML representation of circuit. + */ +const _jsonToHtml = (json: ExecutionPath): string => { + const { qubits, operations } = json; + const { qubitWires, registers, svgHeight } = formatInputs(qubits); + const { metadataList, svgWidth } = processOperations(operations, registers); + const formattedGates: string = formatGates(metadataList); + const measureGates: Metadata[] = metadataList.filter(({ type }) => type === GateType.Measure); + const formattedRegs: string = formatRegisters(registers, measureGates, svgWidth); + return ` + + ${script} + ${style} + ${qubitWires} + ${formattedRegs} + ${formattedGates} + +`; +}; + +export default jsonToHtml; diff --git a/src/Kernel/client/ExecutionPathVisualizer/process.ts b/src/Kernel/client/ExecutionPathVisualizer/process.ts new file mode 100644 index 0000000000..0fce86488e --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/process.ts @@ -0,0 +1,348 @@ +import { + minGateWidth, + startX, + gatePadding, + GateType, + controlBtnOffset, + classicalBoxPadding, +} from "./constants"; +import { Operation } from "./executionPath"; +import { Metadata } 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 Array of registers. + * + * @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 } => { + + // 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(...alignedOps.map(ops => ops.length)); + const columnsWidths: number[] = new Array(numColumns).fill(minGateWidth); + // Keep track of which ops are already seen to avoid duplicate rendering + const visited: { [opIdx: number]: boolean } = {}; + // Unique HTML class for each classically-controlled group of gates. + let cls: number = 1; + + // Map operation index to gate metadata for formatting later + const opsMetadata: Metadata[][] = alignedOps.map((regOps) => + regOps.map((opIdx, col) => { + let op: Operation | null = null; + + if (opIdx != null && !visited.hasOwnProperty(opIdx)) { + op = operations[opIdx]; + visited[opIdx] = true; + } + + const metadata: Metadata = _opToMetadata(op, registers); + + // Add HTML class attribute if classically controlled + if (metadata.type === GateType.ClassicalControlled) { + _addClass(metadata, `cls-control-${cls++}`); + } + + // 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(...Object.keys(registers).map(Number)) + 1; + const groupedOps: number[][] = Array.from(Array(numRegs), () => new Array(0)); + operations.forEach(({ targets, controls }, instrIdx) => { + const qRegs: Register[] = [...controls, ...targets].filter(({ type }) => type === RegisterType.Qubit); + const qRegIdxList: number[] = qRegs.map(({ qId }) => qId); + const clsControls: Register[] = controls.filter(({ type }) => type === RegisterType.Classical); + const isClassicallyControlled: boolean = clsControls.length > 0; + // 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(...ops.map(regOps => regOps.length)); + let col: number = 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(...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; +} + +/** + * Given an array of column widths, calculate the middle x coord of each column. + * This will be used to centre the gates within each column. + * + * @param columnWidths Array of column widths where `columnWidths[i]` is the + * width of the `i`th column. + * + * @returns Object containing the middle x coords of each column (`columnsX`) and the width + * of the corresponding SVG (`svgWidth`). + */ +const _getColumnsX = (columnWidths: number[]): { columnsX: number[], svgWidth: number } => { + const columnsX: number[] = new Array(columnWidths.length).fill(0); + let x: number = startX; + columnWidths.forEach((width, i) => { + columnsX[i] = x + width / 2; + x += width + gatePadding * 2; + }); + return { columnsX, svgWidth: x }; +}; + +/** + * 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: minGateWidth, + }; + + if (op == null) return metadata; + + let { gate, argStr, controlled, adjoint, controls, targets, children } = op; + + // Set y coords + metadata.controlsY = controls.map(reg => _getRegY(reg, registers)); + metadata.targetsY = targets.map(reg => _getRegY(reg, registers)); + + if (children != null && children.length > 0) { + // Classically-controlled operations + + // Gates to display when classical bit is 0. + let childrenInstrs = processOperations(children[0], registers); + const zeroGates: Metadata[] = childrenInstrs.metadataList; + const zeroChildWidth: number = childrenInstrs.svgWidth; + + // Gates to display when classical bit is 1. + childrenInstrs = processOperations(children[1], 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 + classicalBoxPadding * 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); + metadata.targetsY = [Math.min(...qubitsY), Math.max(...qubitsY)]; + } else if (gate === 'measure') { + metadata.type = GateType.Measure; + } else if (gate === 'SWAP') { + metadata.type = GateType.Swap; + } else if (controlled) { + metadata.type = (gate === 'X') ? GateType.Cnot : GateType.ControlledUnitary; + 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 (adjoint && metadata.label.length > 0) metadata.label += "'"; + + // If gate has extra arguments, display them + if (argStr != null) metadata.argStr = argStr; + + // Set gate width + metadata.width = getGateWidth(metadata); + + 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 (!registers.hasOwnProperty(qId)) throw new Error(`ERROR: Qubit register with ID ${qId} not found.`); + const { y, children } = registers[qId]; + switch (type) { + 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}.`); + } +}; + +/** + * Adds HTML class to metadata and its nested children. + * + * @param metadata Metadata assigned to class. + * @param cls HTML class name. + */ +const _addClass = (metadata: Metadata, cls: string): void => { + metadata.htmlClass = cls; + if (metadata.children != null) { + metadata.children[0].forEach(child => _addClass(child, cls)); + metadata.children[1].forEach(child => _addClass(child, cls)); + } +}; + +/** + * 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]; + if (metadata.type === GateType.ClassicalControlled) { + // Subtract startX offset from nested gates and add offset and padding + const offset: number = x - startX + controlBtnOffset + classicalBoxPadding; + + // 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; + } else { + // Get x coord of middle of each column (used for centering gates in a column) + metadata.x = x + columnWidths[col] / 2; + } + })); + + 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[][] | undefined), offset: number): void => { + if (children == null) return; + children.flat().forEach(child => { + child.x += offset; + _offsetChildrenX(child.children, offset); + }); +}; + +export { + processOperations, + _groupOperations, + _alignOps, + _getColumnsX, + _opToMetadata, + _getRegY, + _addClass, + _fillMetadataX, + _offsetChildrenX, +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/register.ts b/src/Kernel/client/ExecutionPathVisualizer/register.ts new file mode 100644 index 0000000000..1a50b130a5 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/register.ts @@ -0,0 +1,32 @@ +/** + * Type of register. + */ +export enum RegisterType { + Qubit, + Classical +}; + +/** + * Represents a register resource. + */ +export interface Register { + /** Type of register. */ + type: RegisterType; + /** Qubit register ID. */ + qId: number; + /** Classical register ID (if classical register). */ + cId?: number; +}; + +export interface RegisterMetadata { + /** Type of register. */ + type: RegisterType; + /** y coord of register */ + y: number; + /** Nested classical registers attached to quantum register. */ + children?: RegisterMetadata[]; +}; + +export interface RegisterMap { + [id: number]: RegisterMetadata +}; diff --git a/src/Kernel/client/ExecutionPathVisualizer/utils.ts b/src/Kernel/client/ExecutionPathVisualizer/utils.ts new file mode 100644 index 0000000000..b2bea8a4a4 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/utils.ts @@ -0,0 +1,53 @@ +import { Metadata } from './metadata'; +import { + GateType, + minGateWidth, + labelPadding, + labelFontSize, + argsFontSize, +} from './constants'; +import fontSizes from './fontSizes'; + +/** + * 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, argStr, width }: Metadata): number => { + switch (type) { + case GateType.ClassicalControlled: + // Already computed before. + return width; + case GateType.Measure: + case GateType.Cnot: + case GateType.Swap: + return minGateWidth; + default: + const labelWidth = _getStringWidth(label); + const argsWidth = (argStr != null) ? _getStringWidth(argStr, 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 str Input string. + * @param fontSize Font size of `str`. + * + * @returns Pixel width of given string. + */ +const _getStringWidth = (str: string, fontSize: number = labelFontSize): number => { + const scale = fontSize / 100; + const unScaledWidth = str.split('').reduce( + (totalLen: number, ch: string) => totalLen + fontSizes[ch][0], 0); + return scale * unScaledWidth; +}; + +export { + getGateWidth, + _getStringWidth, +}; diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts index 7590ddc03b..675387474e 100644 --- a/src/Kernel/client/kernel.ts +++ b/src/Kernel/client/kernel.ts @@ -5,9 +5,10 @@ /// import { IPython } from "./ipython"; -declare var IPython : IPython; +declare var IPython: IPython; import { Telemetry, ClientInfo } from "./telemetry.js"; +import jsonToHtml from "./ExecutionPathVisualizer/pathVisualizer.js"; function defineQSharpMode() { console.log("Loading IQ# kernel-specific extension..."); @@ -116,9 +117,9 @@ function defineQSharpMode() { } class Kernel { - hostingEnvironment : string | undefined; - iqsharpVersion : string | undefined; - telemetryOptOut? : boolean | null; + hostingEnvironment: string | undefined; + iqsharpVersion: string | undefined; + telemetryOptOut?: boolean | null; constructor() { IPython.notebook.kernel.events.on("kernel_ready.Kernel", args => { @@ -147,7 +148,7 @@ class Kernel { // are replies to other messages. IPython.notebook.kernel.send_shell_message( "iqsharp_echo_request", - {value: value}, + { value: value }, { shell: { reply: (message) => { From 4bf11d036e32c5b8af4219ab3c429e4eef4f1af7 Mon Sep 17 00:00:00 2001 From: Raphael Koh Date: Fri, 10 Jul 2020 11:51:34 -0400 Subject: [PATCH 2/4] Fix build errors --- .../client/ExecutionPathVisualizer/index.ts | 193 ------------------ src/Kernel/package-lock.json | 6 + src/Kernel/package.json | 3 +- src/Kernel/tsconfig.json | 2 +- 4 files changed, 9 insertions(+), 195 deletions(-) delete mode 100644 src/Kernel/client/ExecutionPathVisualizer/index.ts diff --git a/src/Kernel/client/ExecutionPathVisualizer/index.ts b/src/Kernel/client/ExecutionPathVisualizer/index.ts deleted file mode 100644 index 9b94bff735..0000000000 --- a/src/Kernel/client/ExecutionPathVisualizer/index.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { ExecutionPath } from "./executionPath"; -import { jsonToSvg, jsonToHtml } from "./pathVisualizer"; - -const exampleJSON: ExecutionPath = { - qubits: [ - { id: 0, numChildren: 1 }, - { id: 1 }, - { id: 2 }, - { id: 3 } - ], - operations: [ - { - "gate": "H", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [{ type: 0, qId: 1 }] - }, - { - "gate": "RX", - "argStr": "(0.25)", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 1 }], - "targets": [{ type: 0, qId: 0 }] - }, - { - "gate": "X", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 1 }], - "targets": [ - { type: 0, qId: 2 }, - { type: 0, qId: 3 } - ] - }, - { - "gate": "X", - "controlled": true, - "adjoint": false, - "controls": [ - { type: 0, qId: 2 }, - { type: 0, qId: 3 } - ], - "targets": [ - { type: 0, qId: 1 } - ] - }, - { - "gate": "X", - "controlled": true, - "adjoint": false, - "controls": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ], - "targets": [{ type: 0, qId: 2 }] - }, - { - "gate": "X", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 2 }], - "targets": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ] - }, - { - "gate": "measure", - "controlled": false, - "adjoint": false, - "controls": [{ type: 0, qId: 0 }], - "targets": [{ type: 1, qId: 0, cId: 0 }] - }, - { - "gate": "if", - "controlled": false, - "adjoint": false, - "controls": [{ type: 1, qId: 0, cId: 0 }], - "targets": [], - children: [[ - { - "gate": "H", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [{ type: 0, qId: 1 }] - }, - { - "gate": "X", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [{ type: 0, qId: 1 }] - } - ], - [ - { - "gate": "X", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 0 }], - "targets": [{ type: 0, qId: 1 }] - }, - { - "gate": "Foo", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [{ type: 0, qId: 3 }] - } - ]] - }, - { - "gate": "SWAP", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [ - { type: 0, qId: 0 }, - { type: 0, qId: 2 } - ] - }, - { - "gate": "ZZ", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ] - }, - { - "gate": "ZZ", - "controlled": false, - "adjoint": false, - "controls": [], - "targets": [ - { type: 0, qId: 0 }, - { type: 0, qId: 1 } - ] - }, - { - "gate": "XX", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 0 }], - "targets": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ] - }, - { - "gate": "XX", - "controlled": true, - "adjoint": false, - "controls": [{ type: 0, qId: 2 }], - "targets": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ] - }, - { - "gate": "XX", - "controlled": true, - "adjoint": false, - "controls": [ - { type: 0, qId: 0 }, - { type: 0, qId: 2 } - ], - "targets": [ - { type: 0, qId: 1 }, - { type: 0, qId: 3 } - ] - }, - ] -}; - -const saveToFile = (fileName: string, contents: string): void => { - const filePath: string = path.join(__dirname, fileName); - fs.writeFile(filePath, contents, 'utf8', (err) => console.log(err)); - -}; - -// Example use case () -const svg = jsonToSvg(exampleJSON); -saveToFile('../example.svg', svg); -const html = jsonToHtml(exampleJSON); -saveToFile('../example.html', html); diff --git a/src/Kernel/package-lock.json b/src/Kernel/package-lock.json index ba38a198ed..885dfe5cc0 100644 --- a/src/Kernel/package-lock.json +++ b/src/Kernel/package-lock.json @@ -7,6 +7,12 @@ "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.56.tgz", "integrity": "sha512-OMtPqg2wFOEcNeVga+m+UXpYJw8ugISPCQOtShdFUho/k91Ms1oWOozoDT1I87Phv6IdwLfMLtIOahh1tO1cJQ==", "dev": true + }, + "@types/node": { + "version": "14.0.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz", + "integrity": "sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g==", + "dev": true } } } diff --git a/src/Kernel/package.json b/src/Kernel/package.json index cbafb82936..f1bc80c2dd 100644 --- a/src/Kernel/package.json +++ b/src/Kernel/package.json @@ -1,5 +1,6 @@ { "devDependencies": { - "@types/codemirror": "0.0.56" + "@types/codemirror": "0.0.56", + "@types/node": "^14.0.12" } } diff --git a/src/Kernel/tsconfig.json b/src/Kernel/tsconfig.json index 520c040ad4..baf6732b26 100644 --- a/src/Kernel/tsconfig.json +++ b/src/Kernel/tsconfig.json @@ -8,7 +8,7 @@ "baseUrl": ".", "outDir": "res", "outFile": "res/bundle.js", - "lib": [ "DOM", "ES2015" ], + "lib": [ "DOM", "ES2019" ], "module": "AMD" }, "exclude": ["node_modules", "wwwroot"], From 5eaae528e82a33ed065f4dba39dcfc7241324ec2 Mon Sep 17 00:00:00 2001 From: Raphael Koh Date: Fri, 10 Jul 2020 17:42:56 -0400 Subject: [PATCH 3/4] Minor fixes addressing PR comments --- .../formatters/gateFormatter.ts | 11 ++++++----- .../client/ExecutionPathVisualizer/pathVisualizer.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts index 4d57440722..5132866b57 100644 --- a/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts @@ -102,13 +102,14 @@ const _unitary = (label: string, x: number, y: number[], width: number, argStr?: // Group adjacent registers let prevY: number = y[0]; - const regGroups: number[][] = y.reduce((acc: number[][], currY: number) => { + const regGroups: number[][] = y.reduce((groups: number[][], currY: number) => { // Registers are defined to be adjacent if they differ by registerHeight in their y coord - // Raphael: Is there a better way of doing this? (i.e. split into y and heights in processOperations) - if (acc.length === 0 || currY - prevY > registerHeight) acc.push([currY]); - else acc[acc.length - 1].push(currY); + // NOTE: This method of group registers by height difference might break if we want to add + // registers with variable heights. + if (groups.length === 0 || currY - prevY > registerHeight) groups.push([currY]); + else groups[groups.length - 1].push(currY); prevY = currY; - return acc; + return groups; }, []); // Render each group as a separate unitary boxes diff --git a/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts index f43bd6c3af..d110f6b716 100644 --- a/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts +++ b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts @@ -100,7 +100,7 @@ const style = ` * * @returns HTML representation of circuit. */ -const _jsonToHtml = (json: ExecutionPath): string => { +const jsonToHtml = (json: ExecutionPath): string => { const { qubits, operations } = json; const { qubitWires, registers, svgHeight } = formatInputs(qubits); const { metadataList, svgWidth } = processOperations(operations, registers); From d7394bbcd1b991f2c8835b7401625c5baef5fa21 Mon Sep 17 00:00:00 2001 From: Raphael Koh Date: Fri, 10 Jul 2020 19:47:46 -0400 Subject: [PATCH 4/4] Replace logic for _getStringWidth with in-browser method --- .../ExecutionPathVisualizer/fontSizes.ts | 587 ------------------ .../formatters/formatUtils.ts | 2 +- .../client/ExecutionPathVisualizer/utils.ts | 18 +- 3 files changed, 11 insertions(+), 596 deletions(-) delete mode 100644 src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts diff --git a/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts b/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts deleted file mode 100644 index e824a064f9..0000000000 --- a/src/Kernel/client/ExecutionPathVisualizer/fontSizes.ts +++ /dev/null @@ -1,587 +0,0 @@ -// Retrieved from https://github.com/adambisek/string-pixel-width/blob/master/src/widthsMap.js - -/** - * Provides a mapping from character to an array of font sizes represented by: - * [normal, bold, italic, bold + italic] - */ -interface FontSizeMap { - [ch: string]: [number, number, number, number]; -}; - -/** - * Pixel width of each character with font-family Arial. - */ -const fontSizes: FontSizeMap = { - "0": [ - 56, - 56, - 56, - 56 - ], - "1": [ - 56, - 56, - 56, - 56 - ], - "2": [ - 56, - 56, - 56, - 56 - ], - "3": [ - 56, - 56, - 56, - 56 - ], - "4": [ - 56, - 56, - 56, - 56 - ], - "5": [ - 56, - 56, - 56, - 56 - ], - "6": [ - 56, - 56, - 56, - 56 - ], - "7": [ - 56, - 56, - 56, - 56 - ], - "8": [ - 56, - 56, - 56, - 56 - ], - "9": [ - 56, - 56, - 56, - 56 - ], - " ": [ - 28, - 28, - 28, - 28 - ], - "!": [ - 28, - 33, - 28, - 33 - ], - "\"": [ - 35, - 47, - 35, - 47 - ], - "#": [ - 56, - 56, - 56, - 56 - ], - "$": [ - 56, - 56, - 56, - 56 - ], - "%": [ - 89, - 89, - 89, - 89 - ], - "&": [ - 67, - 72, - 67, - 72 - ], - "'": [ - 19, - 24, - 19, - 24 - ], - "(": [ - 33, - 33, - 33, - 33 - ], - ")": [ - 33, - 33, - 33, - 33 - ], - "*": [ - 39, - 39, - 39, - 39 - ], - "+": [ - 58, - 58, - 58, - 58 - ], - ",": [ - 28, - 28, - 28, - 28 - ], - "-": [ - 33, - 33, - 33, - 33 - ], - ".": [ - 28, - 28, - 28, - 28 - ], - "/": [ - 28, - 28, - 28, - 28 - ], - ":": [ - 28, - 33, - 28, - 33 - ], - ";": [ - 28, - 33, - 28, - 33 - ], - "<": [ - 58, - 58, - 58, - 58 - ], - "=": [ - 58, - 58, - 58, - 58 - ], - ">": [ - 58, - 58, - 58, - 58 - ], - "?": [ - 56, - 61, - 56, - 61 - ], - "@": [ - 102, - 98, - 102, - 98 - ], - "A": [ - 67, - 72, - 67, - 72 - ], - "B": [ - 67, - 72, - 67, - 72 - ], - "C": [ - 72, - 72, - 72, - 72 - ], - "D": [ - 72, - 72, - 72, - 72 - ], - "E": [ - 67, - 67, - 67, - 67 - ], - "F": [ - 61, - 61, - 61, - 61 - ], - "G": [ - 78, - 78, - 78, - 78 - ], - "H": [ - 72, - 72, - 72, - 72 - ], - "I": [ - 28, - 28, - 28, - 28 - ], - "J": [ - 50, - 56, - 50, - 56 - ], - "K": [ - 67, - 72, - 67, - 72 - ], - "L": [ - 56, - 61, - 56, - 61 - ], - "M": [ - 83, - 83, - 83, - 83 - ], - "N": [ - 72, - 72, - 72, - 72 - ], - "O": [ - 78, - 78, - 78, - 78 - ], - "P": [ - 67, - 67, - 67, - 67 - ], - "Q": [ - 78, - 78, - 78, - 78 - ], - "R": [ - 72, - 72, - 72, - 72 - ], - "S": [ - 67, - 67, - 67, - 67 - ], - "T": [ - 61, - 61, - 61, - 61 - ], - "U": [ - 72, - 72, - 72, - 72 - ], - "V": [ - 67, - 67, - 67, - 67 - ], - "W": [ - 94, - 94, - 94, - 94 - ], - "X": [ - 67, - 67, - 67, - 67 - ], - "Y": [ - 67, - 67, - 67, - 67 - ], - "Z": [ - 61, - 61, - 61, - 61 - ], - "[": [ - 28, - 33, - 28, - 33 - ], - "\\": [ - 28, - 28, - 28, - 28 - ], - "]": [ - 28, - 33, - 28, - 33 - ], - "^": [ - 47, - 58, - 47, - 58 - ], - "_": [ - 56, - 56, - 56, - 56 - ], - "`": [ - 33, - 33, - 33, - 33 - ], - "a": [ - 56, - 56, - 56, - 56 - ], - "b": [ - 56, - 61, - 56, - 61 - ], - "c": [ - 50, - 56, - 50, - 56 - ], - "d": [ - 56, - 61, - 56, - 61 - ], - "e": [ - 56, - 56, - 56, - 56 - ], - "f": [ - 28, - 33, - 28, - 33 - ], - "g": [ - 56, - 61, - 56, - 61 - ], - "h": [ - 56, - 61, - 56, - 61 - ], - "i": [ - 22, - 28, - 22, - 28 - ], - "j": [ - 22, - 28, - 22, - 28 - ], - "k": [ - 50, - 56, - 50, - 56 - ], - "l": [ - 22, - 28, - 22, - 28 - ], - "m": [ - 83, - 89, - 83, - 89 - ], - "n": [ - 56, - 61, - 56, - 61 - ], - "o": [ - 56, - 61, - 56, - 61 - ], - "p": [ - 56, - 61, - 56, - 61 - ], - "q": [ - 56, - 61, - 56, - 61 - ], - "r": [ - 33, - 39, - 33, - 39 - ], - "s": [ - 50, - 56, - 50, - 56 - ], - "t": [ - 28, - 33, - 28, - 33 - ], - "u": [ - 56, - 61, - 56, - 61 - ], - "v": [ - 50, - 56, - 50, - 56 - ], - "w": [ - 72, - 78, - 72, - 78 - ], - "x": [ - 50, - 56, - 50, - 56 - ], - "y": [ - 50, - 56, - 50, - 56 - ], - "z": [ - 50, - 50, - 50, - 50 - ], - "{": [ - 33, - 39, - 33, - 39 - ], - "|": [ - 26, - 28, - 26, - 28 - ], - "}": [ - 33, - 39, - 33, - 39 - ], - "~": [ - 58, - 58, - 58, - 58 - ] -}; - -export default fontSizes; diff --git a/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts index dbceb2275e..7e9556fffe 100644 --- a/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts @@ -49,7 +49,7 @@ export const line = (x1: number, y1: number, x2: number, y2: number, strokeWidth * @returns SVG string for unitary box. */ export const box = (x: number, y: number, width: number, height: number): string => - ``; + ``; /** * Generate the SVG text element from a given text string. diff --git a/src/Kernel/client/ExecutionPathVisualizer/utils.ts b/src/Kernel/client/ExecutionPathVisualizer/utils.ts index b2bea8a4a4..44d8515b23 100644 --- a/src/Kernel/client/ExecutionPathVisualizer/utils.ts +++ b/src/Kernel/client/ExecutionPathVisualizer/utils.ts @@ -6,7 +6,6 @@ import { labelFontSize, argsFontSize, } from './constants'; -import fontSizes from './fontSizes'; /** * Calculate the width of a gate, given its metadata. @@ -35,16 +34,19 @@ const getGateWidth = ({ type, label, argStr, width }: Metadata): number => { /** * Get the width of a string with font-size `fontSize` and font-family Arial. * - * @param str Input string. - * @param fontSize Font size of `str`. + * @param text Input string. + * @param fontSize Font size of `text`. * * @returns Pixel width of given string. */ -const _getStringWidth = (str: string, fontSize: number = labelFontSize): number => { - const scale = fontSize / 100; - const unScaledWidth = str.split('').reduce( - (totalLen: number, ch: string) => totalLen + fontSizes[ch][0], 0); - return scale * unScaledWidth; +const _getStringWidth = (text: string, fontSize: number = labelFontSize): number => { + var canvas: HTMLCanvasElement = document.createElement("canvas"); + var context: CanvasRenderingContext2D | null = canvas.getContext("2d"); + if (context == null) throw new Error("Null canvas"); + + context.font = `${fontSize}px Arial`; + var metrics: TextMetrics = context.measureText(text); + return metrics.width; }; export {