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/formatters/formatUtils.ts b/src/Kernel/client/ExecutionPathVisualizer/formatters/formatUtils.ts new file mode 100644 index 0000000000..7e9556fffe --- /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..5132866b57 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/formatters/gateFormatter.ts @@ -0,0 +1,309 @@ +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((groups: number[][], currY: number) => { + // Registers are defined to be adjacent if they differ by registerHeight in their y coord + // 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 groups; + }, []); + + // 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/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..d110f6b716 --- /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..44d8515b23 --- /dev/null +++ b/src/Kernel/client/ExecutionPathVisualizer/utils.ts @@ -0,0 +1,55 @@ +import { Metadata } from './metadata'; +import { + GateType, + minGateWidth, + labelPadding, + labelFontSize, + argsFontSize, +} from './constants'; + +/** + * 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 text Input string. + * @param fontSize Font size of `text`. + * + * @returns Pixel width of given string. + */ +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 { + 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) => { 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"],