diff --git a/frontend/src/workspace/components/download/DownloadButton.js b/frontend/src/workspace/components/download/DownloadButton.js index 0553bbad..57eabd4b 100644 --- a/frontend/src/workspace/components/download/DownloadButton.js +++ b/frontend/src/workspace/components/download/DownloadButton.js @@ -4,8 +4,6 @@ import Template from '../../plaquettes/Template'; import config from './config'; import notification from '../notification'; -const assert = require('assert'); - export default class DownloadButton extends Button { constructor( workspace, @@ -44,16 +42,12 @@ export default class DownloadButton extends Button { .toSorted((a, b) => a.globalX - b.globalX) // leftmost qubits .toSorted((a, b) => a.globalY - b.globalY)[0]; // topmost qubit plaquette.qubits.forEach((qubit) => { - assert( - qubit.qubitType === 'data' || qubit.qubitType === 'syndrome', - "Qubit type must be either 'data' or 'syndrome'", - ); marshalledPlaquette.qubits.push({ x: (originQubit.globalX - qubit.globalX) / this.workspace.gridSize, y: (originQubit.globalY - qubit.globalY) / this.workspace.gridSize, - qubitType: qubit.qubitType, + qubitType: qubit.label, }); }); payload.plaquettes.push(marshalledPlaquette); diff --git a/frontend/src/workspace/graphics/Grid.js b/frontend/src/workspace/graphics/Grid.js index 038c3da3..47b28b6b 100644 --- a/frontend/src/workspace/graphics/Grid.js +++ b/frontend/src/workspace/graphics/Grid.js @@ -59,7 +59,6 @@ export default class Grid extends Graphics { contains = (qubits) => { const minX = Math.min(...this.visibleUnits().map((unit) => unit.x)); const minY = Math.min(...this.visibleUnits().map((unit) => unit.y)); - console.log('Min x, min y', minX, minY); const unitXs = new Set(this.visibleUnits().map((unit) => [unit.x, unit.x + this.gridSize]).flat()); const unitYs = new Set(this.visibleUnits().map((unit) => [unit.y, unit.y + this.gridSize]).flat()); for (const qubit of qubits) { diff --git a/frontend/src/workspace/graphics/GridUnit.js b/frontend/src/workspace/graphics/GridUnit.js index e63f9b4b..89bcce12 100644 --- a/frontend/src/workspace/graphics/GridUnit.js +++ b/frontend/src/workspace/graphics/GridUnit.js @@ -43,7 +43,6 @@ export default class GridUnit extends Graphics { } // Check whether the click was within the bounds of the grid unit if (relativeX === this.x && relativeY === this.y) { - console.log('Toggling visibility of ', this.name); this.visible = !this.visible; } }; diff --git a/frontend/src/workspace/plaquettes/Circuit.js b/frontend/src/workspace/plaquettes/Circuit.js deleted file mode 100644 index eb4feaa3..00000000 --- a/frontend/src/workspace/plaquettes/Circuit.js +++ /dev/null @@ -1 +0,0 @@ -export default class CircuitBuilder {} diff --git a/frontend/src/workspace/plaquettes/Plaquette.js b/frontend/src/workspace/plaquettes/Plaquette.js index 4f5508dd..a95d4af0 100644 --- a/frontend/src/workspace/plaquettes/Plaquette.js +++ b/frontend/src/workspace/plaquettes/Plaquette.js @@ -1,16 +1,23 @@ /* eslint-disable no-param-reassign */ -import { Graphics, Container, Color } from 'pixi.js'; +import { + Graphics, Container, Color, RoundedRectangle +} from 'pixi.js'; import Button from '../components/Button'; +import notification from '../components/notification'; +import { CircuitLabels } from '../qubits/Qubit'; +import createCircuitAsciiArt from './circuit'; export const PlaquetteColors = { purple: new Color('purple'), yellow: new Color('yellow'), }; + export default class Plaquette extends Graphics { - constructor(qubits, workspace, color = PlaquetteColors.purple) { + constructor(qubits, workspace, app, template, color = PlaquetteColors.purple) { super(); // UI properties this.workspace = workspace; + this.app = app; this.color = color; this.gridSize = workspace.gridSize; this.gridOffsetX = this.gridSize; @@ -18,6 +25,8 @@ export default class Plaquette extends Graphics { this.isDragging = false; this.plaquetteMade = false; this.name = 'plaquette'; + this.template = template; + this.circuitASCII = undefined; // Control panel properties this.controlPanel = new Container(); @@ -53,7 +62,7 @@ export default class Plaquette extends Graphics { this.initializeNewButton(this.newButtonLeft, 'new_button_left'); this.initializeNewButton(this.newButtonBottom, 'new_button_bottom'); - this.makeRotatable(); // Add the rotate button to screen + // this.makeRotatable(); // Add the rotate button to screen this.initializeClearButton(); // Add the clearbutton to the screen this.changeColorButton(); // Add the change color button to the screen @@ -66,9 +75,124 @@ export default class Plaquette extends Graphics { // Make this plaquette's qubits invisible this.qubits.forEach((qubit) => { qubit.hideQubitLabels(); + qubit.zIndex = 1; + }); + + this.zIndex = 1; // Make the plaquette appear below of the and labels + + // Specify circuit button + this.specifyCircuitButton = new Button('Specify canonical circuit', 200, 725, 'black'); + this.controlPanel.addChild(this.specifyCircuitButton); + this.specifyCircuitButton.on('click', () => { + notification(this.app, 'Step 1: Specify ancilla qubit'); + app.view.addEventListener('click', this.selectAncillaQubit); + this.qubits.forEach((allQubit) => { + allQubit.zIndex = 2; + }); + this.workspace.sortChildren(); + }); + + // When the mouse hovers over the plaquette, show the circuit if it exists + this.on('mouseover', () => { + if (this.circuitASCIIRectangle) { + this.circuitASCIIRectangle.visible = true; + } + }); + this.on('mouseout', () => { + if (this.circuitASCIIRectangle) { + this.circuitASCIIRectangle.visible = false; + } }); } + circuitASCIIRect = (str) => { + const canvasRect = this.app.view.getBoundingClientRect(); + // Add rectangle to top right of the canvas + const x = canvasRect.right - 200; + const y = canvasRect.top; + const width = 200; + const height = 200; + const rectangle = new RoundedRectangle(x, y, width, height); + rectangle.label = str; + return rectangle; + }; + + getRelativeXY = (e) => { + const canvasRect = this.app.view.getBoundingClientRect(); // Get canvas position + // Calculate the relative click position within the canvas + const relativeX = e.clientX - canvasRect.left; + const relativeY = e.clientY - canvasRect.top; + return { relativeX, relativeY }; + }; + + getQubit = (relativeX, relativeY) => this.qubits.find( + (q) => q.checkHitArea(relativeX, relativeY) + ); + + selectAncillaQubit = (e) => { + const { relativeX, relativeY } = this.getRelativeXY(e); + const qubit = this.getQubit(relativeX, relativeY); + if (!qubit) return; + qubit.setCircuitLabel(CircuitLabels.ancilla); + qubit.showLabelText(); + this.app.view.removeEventListener('click', this.selectAncillaQubit); + notification(this.app, 'Step 2: Specify measure qubit'); + this.app.view.addEventListener('click', this.selectMeasureQubit); + }; + + selectMeasureQubit = (e) => { + const { relativeX, relativeY } = this.getRelativeXY(e); + const qubit = this.getQubit(relativeX, relativeY); + if (!qubit) return; + qubit.setCircuitLabel(CircuitLabels.measure); + qubit.showLabelText(); + this.app.view.removeEventListener('click', this.selectMeasureQubit); + notification(this.app, 'Step 3: Specify CX qubits'); + this.app.view.addEventListener('click', this.selectCXQubit); + this.cxDoneButton = new Button('CX Done', 200, 125, 'black'); + this.controlPanel.addChild(this.cxDoneButton); + this.cxDoneButton.on('click', () => { + this.controlPanel.removeChild(this.cxDoneButton); + this.app.view.removeEventListener('click', this.selectCXQubit); + notification(this.app, 'Step 4: Specify CZ qubits'); + this.app.view.addEventListener('click', this.selectCZQubit); + this.czDoneButton = new Button('CZ Done', 200, 125, 'black'); + this.controlPanel.addChild(this.czDoneButton); + this.czDoneButton.on('click', () => { + this.controlPanel.removeChild(this.czDoneButton); + this.app.view.removeEventListener('click', this.selectCZQubit); + this.controlPanel.removeChild(this.DoneButton); + this.qubits.forEach((allQubit) => { + allQubit.hideQubitLabels(); + }); + const dataQubits = this.qubits.filter( + (_qubit) => _qubit.label === CircuitLabels.cx || _qubit.label === CircuitLabels.cz + ); + const ancillaQubit = this.qubits.find((_qubit) => _qubit.label === CircuitLabels.ancilla); + const circuit = createCircuitAsciiArt(dataQubits, ancillaQubit, true, 'cnot'); + this.circuitASCIIRectangle = this.circuitASCIIRect(circuit); + console.log(circuit); + // this.workspace.addChild(this.circuitASCIIRectangle); + }); + }); + }; + + selectCXQubit = (e) => { + const { relativeX, relativeY } = this.getRelativeXY(e); + const qubit = this.getQubit(relativeX, relativeY); + if (!qubit) return; + qubit.setCircuitLabel(CircuitLabels.cx); + qubit.showLabelText(); + }; + + selectCZQubit = (e) => { + const { relativeX, relativeY } = this.getRelativeXY(e); + const qubit = this.getQubit(relativeX, relativeY); + if (!qubit) return; + qubit.setCircuitLabel(CircuitLabels.cz); + qubit.showLabelText(); + }; + // Get the leftmost qubit mostLeftQubit = () => this.qubitStack.toSorted((a, b) => a.globalX - b.globalX)[0]; @@ -82,7 +206,7 @@ export default class Plaquette extends Graphics { mostBottomQubit = () => this.qubitStack.toSorted((a, b) => b.globalY - a.globalY)[0]; calculatePlaquetteCenter = () => { - // Get the geometric center of the plaquette + // Get the geometric center of the plaquette if (this.qubitStack.length === 0) { return { x: 0, y: 0 }; // Return the center of an empty polygon as (0, 0). } @@ -102,16 +226,16 @@ export default class Plaquette extends Graphics { }; // Function to snap coordinates to the grid - snapToGrid(x, y) { + snapToGrid = (x, y) => { const snappedX = Math.round((x - this.gridOffsetX) / this.gridSize) * this.gridSize + this.gridOffsetX; const snappedY = Math.round((y - this.gridOffsetY) / this.gridSize) * this.gridSize + this.gridOffsetY; return { x: snappedX, y: snappedY }; - } + }; createConvexHull = () => { - // Create a convex hull using grahams scan credit: https://en.wikipedia.org/wiki/Graham_scan + // Create a convex hull using grahams scan credit: https://en.wikipedia.org/wiki/Graham_scan const qubitStack = []; // Sort the qubits by the leftmost qubit const sortedQubits = this.qubits.toSorted((a, b) => a.globalX - b.globalX); @@ -127,7 +251,7 @@ export default class Plaquette extends Graphics { // Sort points by polar angle with respect to lowestYQubit let sortedPoints = sortedQubits.toSorted((a, b) => { - // Calculate the polar angle + // Calculate the polar angle const polarAngleA = Math.atan2( a.globalY - lowestYQubit.globalY, a.globalX - lowestYQubit.globalX, @@ -154,7 +278,7 @@ export default class Plaquette extends Graphics { }); sortedPoints = sortedPoints.filter((qubit) => { - // If the qubit is the lowestYQubit, then keep it + // If the qubit is the lowestYQubit, then keep it if (qubit === lowestYQubit) return qubit; // Get the qubits that have the same polar angle const samepQubits = sortedPoints.filter((q) => q.pAngle === qubit.pAngle); @@ -166,8 +290,8 @@ export default class Plaquette extends Graphics { // Create the stack of qubits which contains the convex hull sortedPoints.forEach((qubit) => { - // Remove qubits from the stack if the angle formed by points next-to-top, top, - // and qubit is not counterclockwise + // Remove qubits from the stack if the angle formed by points next-to-top, top, + // and qubit is not counterclockwise while ( qubitStack.length > 1 && this.ccw( @@ -200,7 +324,7 @@ export default class Plaquette extends Graphics { // eslint-disable-next-line class-methods-use-this ccw = (p, q, r) => { - // Check if the points are counterclockwise or collinear + // Check if the points are counterclockwise or collinear const v1 = { x: q.globalX - p.globalX, y: q.globalY - p.globalY }; const v2 = { x: r.globalX - q.globalX, y: r.globalY - q.globalY }; // Take the cross product. @@ -210,8 +334,8 @@ export default class Plaquette extends Graphics { }; populatePlaquetteShapeAndInteractionFields = () => { - // The graphic should connect the points from the qubits - // Create a convex hull + // The graphic should connect the points from the qubits + // Create a convex hull this.createConvexHull(); const { x, y } = this.calculatePlaquetteCenter(); // Set the position and pivot of the plaquette @@ -226,17 +350,17 @@ export default class Plaquette extends Graphics { }; makeExtensible = () => { - // Make the plaquette extensible + // Make the plaquette extensible this.buttonMode = true; this.on('pointerdown', this.onDragStart); this.on('pointermove', this.onDragMove); this.on('pointerup', this.onDragEnd); }; - onDragStart(event) { + onDragStart = (event) => { this.isDragging = true; this.initialPosition = event.data.getLocalPosition(this.parent); - } + }; onDragMove = (event) => { if (!this.isDragging) { @@ -256,11 +380,11 @@ export default class Plaquette extends Graphics { const threshold = this.gridSize * scaleFactor; // If the cursor is moved meaningfully, check which direction to copy to if (Math.abs(deltaX) >= threshold || Math.abs(deltaY) >= threshold) { - // Check which direction the plaquette is being dragged + // Check which direction the plaquette is being dragged if (Math.abs(deltaX) > Math.abs(deltaY)) { - // Dragging horizontally + // Dragging horizontally if (deltaX > 0) { - // Moving to the right + // Moving to the right shiftQ = this.mostRightQubit(); // Find the neighboring qubit that is closest to the right const newrq = shiftQ.neighbors.find( @@ -269,8 +393,8 @@ export default class Plaquette extends Graphics { diff = newrq.globalX - shiftQ.globalX; } else { - // Moving to the left - // Generate the qubits that are closest to the left + // Moving to the left + // Generate the qubits that are closest to the left shiftQ = this.mostLeftQubit(); // Find the neighboring qubit that is closest to the left const newlq = shiftQ.neighbors.find( @@ -281,14 +405,14 @@ export default class Plaquette extends Graphics { // Shift the qubits by the difference this.qubits.forEach((qubit) => { const shiftedQubit = qubit.neighbors.find( - // eslint-disable-next-line no-loop-func + // eslint-disable-next-line no-loop-func (q) => q.globalX === qubit.globalX + diff && q.globalY === qubit.globalY, ); newQubits.push(shiftedQubit); }); } else { if (deltaY > 0) { - // Generate the qubits that are closest to the top + // Generate the qubits that are closest to the top shiftQ = this.mostTopQubit(); // Find the neighboring qubit that is closest to the top const newtq = shiftQ.neighbors.find( @@ -297,7 +421,7 @@ export default class Plaquette extends Graphics { diff = newtq.globalY - shiftQ.globalY; } else { - // Generate the qubits that are closest to the bottom + // Generate the qubits that are closest to the bottom shiftQ = this.mostBottomQubit(); // Find the neighboring qubit that is closest to the bottom const newbq = shiftQ.neighbors.find( @@ -318,31 +442,31 @@ export default class Plaquette extends Graphics { } }; - createNewPlaquette(newQubits) { + createNewPlaquette = (newQubits) => { const newColor = this.color === PlaquetteColors.purple ? PlaquetteColors.yellow : PlaquetteColors.purple; - const newPlaquette = new Plaquette(newQubits, this.workspace, newColor); + const newPlaquette = new Plaquette(newQubits, this.workspace, this.app, newColor); return newPlaquette; - } + }; - changePlaquetteColor(newColor) { + changePlaquetteColor = (newColor) => { this.color = newColor; // Update the color of the convex hull shape. this.clear(); this.populatePlaquetteShapeAndInteractionFields(); - } + }; - onDragEnd() { + onDragEnd = () => { this.isDragging = false; this.initialPosition = null; - } + }; toggleCtrlButtons = () => { this.on('click', () => { const currentTime = Date.now(); if (currentTime - this.lastClickTime < 300) { - // Tell the workspace that the current control panel should be this one. + // Tell the workspace that the current control panel should be this one. this.workspace.updateSelectedPlaquette(this); } this.lastClickTime = currentTime; @@ -350,9 +474,9 @@ export default class Plaquette extends Graphics { }; makeRotatable = () => { - // Rotate the plaquette by 90 degrees. Still needs to make sure it maps to qubits on grid + // Rotate the plaquette by 90 degrees. Still needs to make sure it maps to qubits on grid this.rotateButton.on('click', () => { - // Rotate the plaquette + // Rotate the plaquette this.rotation += Math.PI / 2; // Rotate 90 degrees }); this.rotateButton.name = 'rotate_button'; @@ -362,7 +486,7 @@ export default class Plaquette extends Graphics { changeColorButton = () => { this.colorButton.on('click', () => { - // Change the color of the plaquette + // Change the color of the plaquette if (this.color === PlaquetteColors.purple) { this.changePlaquetteColor(PlaquetteColors.yellow); } else { @@ -433,7 +557,7 @@ export default class Plaquette extends Graphics { // eslint-disable-next-line no-restricted-syntax for (const qubit of this.qubits) { const shiftedQubit = qubit.neighbors.find( - // eslint-disable-next-line no-loop-func + // eslint-disable-next-line no-loop-func (q) => q.globalX === qubit.globalX + diff && q.globalY === qubit.globalY, ); newQubits.push(shiftedQubit); @@ -442,7 +566,7 @@ export default class Plaquette extends Graphics { }; clonedBottomQubits = () => { - // Generate the qubits that are closest to the top + // Generate the qubits that are closest to the top const newQubits = []; const shiftQ = this.mostTopQubit(); // Find the neighboring qubit that is closest to the top diff --git a/frontend/src/workspace/plaquettes/Template.js b/frontend/src/workspace/plaquettes/Template.js index 1480b24b..93acdbc1 100644 --- a/frontend/src/workspace/plaquettes/Template.js +++ b/frontend/src/workspace/plaquettes/Template.js @@ -262,8 +262,12 @@ export default class Template { return; } // Render the plaquette - const plaquette = new Plaquette(this.selectedQubits, this.workspace); - if (!plaquette.plaquetteMade); + const plaquette = new Plaquette(this.selectedQubits, this.workspace, this.app); + // Remove seleected qubits from the template qubits, so they can be + // selected for circuit construction. + this.templateQubits = this.templateQubits.filter( + (qubit) => !this.selectedQubits.includes(qubit) + ); // Add the plaquette to the tile container this.container.addChild(plaquette); // For each qubit, remove the text diff --git a/frontend/src/workspace/plaquettes/circuit.js b/frontend/src/workspace/plaquettes/circuit.js new file mode 100644 index 00000000..121f684e --- /dev/null +++ b/frontend/src/workspace/plaquettes/circuit.js @@ -0,0 +1,122 @@ +/* eslint-disable no-tabs */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable no-shadow */ +/* eslint-disable no-plusplus */ +/* eslint-disable no-mixed-spaces-and-tabs */ +/* eslint-disable no-param-reassign */ +/* eslint-disable camelcase */ + +import { CircuitLabels } from '../qubits/Qubit'; + +/** + * Create the circuit as ASCII art. + * + * Two forms of circuits are possible: + * + * [] "cnot" + * - Hadamard on the data qubits + * - only CNOT as 2q gates + * - every CNOT is controlled by a data qubit and targets the ancilla + * + * [] "univ" + * - Hadamard on the ancilla qubit + * - both CNOT and CZ as 2q gates + * - every CNOT/CZ is controlled by the ancilla and targets a data qubit + */ +export default function createCircuitAsciiArt(data_qubits, anc_qubit, with_time = true, form = 'univ') { + if (form !== 'cnot') form = 'univ'; // Failsafe case of an unknown form option. + const lines = []; + // Add lines for every data qubit. + let idx = 0; + const { cx, cz } = CircuitLabels; + data_qubits.forEach((qubit) => { + let line = `${qubit.name}: ----`; + // Local change of basis + if (form === 'cnot') { + if (qubit.label === cx) line += '-H--'; + if (qubit.label === cz) line += '----'; + } else if (form === 'univ') { + line += '----'; + } + // Previous CNOTs + for (let i = 0; i < idx; i++) { + line += '-|--'; + } + // Current CNOT + if (form === 'cnot') { + line += '-*--'; + } else if (form === 'univ') { + if (qubit.label === cx) line += '-X--'; + if (qubit.label === cz) line += '-*--'; + } + // Next CNOTs + for (let i = idx + 1; i < data_qubits.length; i++) { + line += '----'; + } + // Change of basis + if (form === 'cnot') { + if (qubit.label === cx) line += '-H--'; + if (qubit.label === cz) line += '----'; + } else if (form === 'univ') { + line += '----'; + } + // End + line += '----'; + lines.push(line); + + // Empty line with only vertical bars. + // Q(__,__): ---- + line = ' '; + line += ' '; + // Previous CNOTs and current one + for (let i = 0; i < idx + 1; i++) { + line += ' | '; + } + // Next CNOTs + for (let i = idx + 1; i < data_qubits.length; i++) { + line += ' '; + } + line += ' '; + lines.push(line); + idx += 1; + }); + // Add line for the ancilla qubit. + let line = `${anc_qubit.name}: |0> `; + if (form === 'cnot') { + line += '----'; // During the change of basis + } else if (form === 'univ') { + line += '-H--'; + } + for (let i = 0; i < data_qubits.length; i++) { + if (form === 'cnot') { + line += '-X--'; + } else if (form === 'univ') { + line += '-*--'; + } + } + if (form === 'cnot') { + line += '---- D~ '; + } else if (form === 'univ') { + line += '-H-- D~ '; + } + lines.push(line); + // Add time ruler. + if (with_time) { + // Q(__,__): + line = ' '; + let line2 = 'time ruler'; + for (let i = 0; i < data_qubits.length + 4; i++) { + line += '___ '; + line2 += ` ${i} `; + } + lines.push(line); + lines.push(line2); + } + // Create the message + let art = ''; + lines.slice(0, -1).forEach((line) => { + art = `${art + line}\n`; + }); + art += lines[lines.length - 1]; + return art; +} diff --git a/frontend/src/workspace/qubits/Qubit.js b/frontend/src/workspace/qubits/Qubit.js index 3a6af656..b2769835 100644 --- a/frontend/src/workspace/qubits/Qubit.js +++ b/frontend/src/workspace/qubits/Qubit.js @@ -1,7 +1,12 @@ /* eslint-disable no-param-reassign */ import { Graphics, Text } from 'pixi.js'; -// const assert = require('assert'); +export const CircuitLabels = Object.freeze({ + ancilla: 'A', + measure: 'M', + cx: 'CX', + cz: 'CZ' +}); /** * Qubit class @@ -17,7 +22,6 @@ export default class Qubit extends Graphics { radius = 5, gridSize = 50, color = 'black', - qubitType = 'data' ) { super(); // UI properties @@ -33,7 +37,6 @@ export default class Qubit extends Graphics { this.maxNeighborDist = 2 * this.gridSize; this.neighbors = []; this.gridSize = gridSize; - this.qubitType = qubitType; this.name = `Qubit(${x}, ${y})`; // Adjacent (degree 1) qubits @@ -181,4 +184,22 @@ export default class Qubit extends Graphics { this.bbX = x; this.bbY = y; }; + + setCircuitLabel = (label) => { + this.label = label; + const text = new Text(this.label, { + fill: 'white', + fontSize: 10 + }); + text.anchor.set(0.5); + text.position.set(this.globalX, this.globalY - 10); + text.visible = false; + text.zIndex = 2; + this.addChild(text); + this.text = text; + }; + + showLabelText = () => { + this.text.visible = true; + }; }