From 5491029ec6edb2d27bd4ce4f16088474cd41f8be Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Wed, 29 May 2024 21:32:33 -0700 Subject: [PATCH] g reverse layers, f flip R/M, show/hide examples, color code example, timeline viewer left justified --- glue/crumble/README.md | 8 +-- glue/crumble/crumble.html | 29 ++++++++--- glue/crumble/draw/main_draw.js | 2 +- glue/crumble/draw/timeline_viewer.js | 39 +++++++++----- glue/crumble/editor/editor_state.js | 7 +-- glue/crumble/keyboard/toolbox.js | 30 +++++++++-- glue/crumble/main.js | 78 +++++++++++++++++++++++----- 7 files changed, 149 insertions(+), 44 deletions(-) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index 0afbc545c..16ae6ab63 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -105,8 +105,8 @@ button (now labelled "Hide Import/Export") again. - `e`: Move to next layer. - `q`: Move to previous layer. -- `shift+e`: Move forward 10 layers. -- `shift+q`: Move backward 10 layers. +- `shift+e`: Move forward 5 layers. +- `shift+q`: Move backward 5 layers. - `escape`: Unselect. Set current selection to the empty set. - `delete`: Delete gates at current selection. - `backspace`: Delete gates at current selection. @@ -119,8 +119,8 @@ button (now labelled "Hide Import/Export") again. - `ctrl+c`: Copy selection to clipboard (or entire layer if nothing selected). - `ctrl+v`: Past clipboard contents at current selection (or entire layer if nothing selected). - `ctrl+x`: Cut selection to clipboard (or entire layer if nothing selected). -- `f`: Flip qubit order of selected operations (e.g. flip the control-to-target direction of a CNOT). -- `shift+f`: Flip order of all circuit layers, starting from the current layer until the next empty layer. +- `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT). +- `g`: Reverse order of circuit layers, from the current layer to the next empty layer. - `home`: Jump to the first layer of the circuit. - `end`: Jump to the last layer of the circuit. - `t`: Rotate circuit 45 degrees clockwise. diff --git a/glue/crumble/crumble.html b/glue/crumble/crumble.html index 5dfda322b..f88310094 100644 --- a/glue/crumble/crumble.html +++ b/glue/crumble/crumble.html @@ -10,17 +10,34 @@
Crumble is a prototype stabilizer circuit editor.
- Read the manual

- Load example: surface code (d=3,r=2)
- Load example: bacon shor code (d=7,r=2)
- Load example: three coupler surface code (d=7,r=4)
- Load example: surface code Y basis transition round (d=7)
+
+
+ Read the manual +
+
- +
+
diff --git a/glue/crumble/draw/main_draw.js b/glue/crumble/draw/main_draw.js index 608a5f112..c0580ca54 100644 --- a/glue/crumble/draw/main_draw.js +++ b/glue/crumble/draw/main_draw.js @@ -369,7 +369,7 @@ function draw(ctx, snap) { }); }); - drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords); + drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length); // Draw scrubber. ctx.save(); diff --git a/glue/crumble/draw/timeline_viewer.js b/glue/crumble/draw/timeline_viewer.js index 36ed924f3..2d7640f8f 100644 --- a/glue/crumble/draw/timeline_viewer.js +++ b/glue/crumble/draw/timeline_viewer.js @@ -96,8 +96,9 @@ function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, * @param {!StateSnapshot} snap * @param {!Map} propagatedMarkerLayers * @param {!function(!int): ![!number, !number]} timesliceQubitCoordsFunc + * @param {!int} numLayers */ -function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc) { +function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc, numLayers) { let w = Math.floor(ctx.canvas.width / 2); let qubits = snap.timelineQubits(); @@ -133,32 +134,45 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun } let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25); + let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch); + let min_t_free = snap.curLayer - num_cols_half + 1; + let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1)); + let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers); + let t2t = t => { + let dt = t - snap.curLayer; + dt -= min_t_clamp - min_t_free; + return dt*x_pitch; + } let coordTransform_t = ([x, y, t]) => { let key = `${x},${y}`; if (!base_y2xy.has(key)) { return [undefined, undefined]; } let [xb, yb] = base_y2xy.get(key); - return [xb + (t - snap.curLayer)*x_pitch, yb]; + return [xb + t2t(t), yb]; }; let qubitTimeCoords = (q, t) => { let [x, y] = timesliceQubitCoordsFunc(q); return coordTransform_t([x, y, t]); } - let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch); - let min_t = Math.max(0, snap.curLayer - num_cols_half + 1); - let max_t = snap.curLayer + num_cols_half + 2; ctx.save(); try { ctx.clearRect(w, 0, w, ctx.canvas.height); + + // Draw colored indicators showing Pauli propagation. let hitCounts = new Map(); for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t, max_t, x_pitch, hitCounts); + drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t_clamp, max_t, x_pitch, hitCounts); } + + // Draw highlight of current layer. ctx.globalAlpha *= 0.5; ctx.fillStyle = 'black'; - ctx.fillRect(w*1.5 - rad*1.3, 0, x_pitch, ctx.canvas.height); + { + let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2; + ctx.fillRect(x1, 0, x_pitch, ctx.canvas.height); + } ctx.globalAlpha *= 2; ctx.strokeStyle = 'black'; @@ -166,7 +180,7 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun // Draw wire lines. for (let q of qubits) { - let [x0, y0] = qubitTimeCoords(q, min_t - 1); + let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); let [x1, y1] = qubitTimeCoords(q, max_t + 1); ctx.beginPath(); ctx.moveTo(x0, y0); @@ -174,17 +188,18 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun ctx.stroke(); } - // Draw labels. + // Draw wire labels. ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; for (let q of qubits) { - let [x, y] = qubitTimeCoords(q, min_t - 1); + let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); let qx = snap.circuit.qubitCoordData[q * 2]; let qy = snap.circuit.qubitCoordData[q * 2 + 1]; ctx.fillText(`${qx},${qy}:`, x, y); } - for (let time = min_t; time <= max_t; time++) { + // Draw layers of gates. + for (let time = min_t_clamp; time <= max_t; time++) { let qubitsCoordsFuncForLayer = q => qubitTimeCoords(q, time); let layer = snap.circuit.layers[time]; if (layer === undefined) { @@ -198,7 +213,7 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun // Draw links to timeslice viewer. ctx.globalAlpha = 0.5; for (let q of qubits) { - let [x0, y0] = qubitTimeCoords(q, min_t - 1); + let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); let [x1, y1] = timesliceQubitCoordsFunc(q); if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) { ctx.beginPath(); diff --git a/glue/crumble/editor/editor_state.js b/glue/crumble/editor/editor_state.js index 5a934392f..bb72ec140 100644 --- a/glue/crumble/editor/editor_state.js +++ b/glue/crumble/editor/editor_state.js @@ -215,11 +215,12 @@ class EditorState { /** * @param {!function(!number, !number): ![!number, !number]} coordTransform * @param {!boolean} preview + * @param {!boolean} moveFocus */ - applyCoordinateTransform(coordTransform, preview) { + applyCoordinateTransform(coordTransform, preview, moveFocus) { let c = this.copyOfCurCircuit(); c = c.afterCoordTransform(coordTransform); - if (!preview) { + if (!preview && moveFocus) { let trans = m => { let new_m = new Map(); for (let [x, y] of m.values()) { @@ -244,7 +245,7 @@ class EditorState { this.applyCoordinateTransform((x, y) => { [x, y] = t1(x, y); return t2(x, y); - }, preview); + }, preview, true); } /** diff --git a/glue/crumble/keyboard/toolbox.js b/glue/crumble/keyboard/toolbox.js index 5852e019d..cae74d36d 100644 --- a/glue/crumble/keyboard/toolbox.js +++ b/glue/crumble/keyboard/toolbox.js @@ -161,12 +161,34 @@ function drawToolbox(ev) { let cy = PAD + PITCH * p + DIAM / 2; if (text.startsWith('P')) { ctx.beginPath(); - ctx.moveTo(cx + PITCH * 0.25, cy + PITCH * 0.25); - ctx.lineTo(cx, cy - PITCH * 0.25); - ctx.lineTo(cx - PITCH * 0.25, cy + PITCH * 0.25); + let numPoints = 3; + if (text === 'PX') { + numPoints = 4; + ctx.fillStyle = 'red'; + } else if (text === 'PY') { + numPoints = 5; + ctx.fillStyle = 'green'; + cy += 1; + } else if (text === 'PZ') { + numPoints = 3; + ctx.fillStyle = 'blue'; + cy += 2; + } + let pts = []; + for (let k = 0; k < numPoints; k++) { + let t = 2 * Math.PI / numPoints * (k + 0.5); + pts.push([ + Math.round(cx + 0.3 * DIAM * Math.sin(t)), + Math.round(cy + 0.3 * DIAM * Math.cos(t)), + ]); + } + ctx.moveTo(pts[pts.length - 1][0], pts[pts.length - 1][1]); + for (let pt of pts) { + ctx.lineTo(pt[0], pt[1]); + } + ctx.closePath(); ctx.globalAlpha *= 0.25; - ctx.fillStyle = text === 'PX' ? 'red' : text === 'PY' ? 'green' : 'blue'; ctx.fill(); ctx.globalAlpha *= 4; continue; diff --git a/glue/crumble/main.js b/glue/crumble/main.js index eae094989..bfa1d8a8d 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -27,6 +27,8 @@ const txtStimCircuit = /** @type {!HTMLTextAreaElement} */ document.getElementBy const btnTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnTimelineFocus'); const btnClearTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearTimelineFocus'); const btnClearSelectedMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearSelectedMarkers'); +const btnShowExamples = /** @type {!HTMLButtonElement} */ document.getElementById('btnShowExamples'); +const divExamples = /** @type{!HTMLDivElement} */ document.getElementById('examples-div'); // Prevent typing in the import/export text editor from causing changes in the main circuit editor. txtStimCircuit.addEventListener('keyup', ev => ev.stopPropagation()); @@ -77,6 +79,16 @@ btnClearSelectedMarkers.addEventListener('click', _ev => { editorState.force_redraw(); }); +btnShowExamples.addEventListener('click', _ev => { + if (divExamples.style.display === 'none') { + divExamples.style.display = 'block'; + btnShowExamples.textContent = "Hide Example Circuits"; + } else { + divExamples.style.display = 'none'; + btnShowExamples.textContent = "Show Example Circuits"; + } +}); + btnClearTimelineFocus.addEventListener('click', _ev => { editorState.timelineSet = new Map(); editorState.force_redraw(); @@ -244,18 +256,42 @@ function makeChordHandlers() { let newCircuit = editorState.copyOfCurCircuit(); let layer = newCircuit.layers[editorState.curLayer]; let flipped_op_first_targets = new Set(); + let pairs = [ + ['CX', 'reverse'], + ['CY', 'reverse'], + ['XCY', 'reverse'], + ['CXSWAP', 'reverse'], + ['XCZ', 'reverse'], + ['XCY', 'reverse'], + ['YCX', 'reverse'], + ['SWAPCX', 'reverse'], + ['RX', 'MX'], + ['R', 'M'], + ['RY', 'MY'], + ]; + let rev = new Map(); + for (let p of pairs) { + rev.set(p[0], p[1]); + rev.set(p[1], p[0]); + } for (let q of editorState.focusedSet.keys()) { let op = layer.id_ops.get(newCircuit.coordToQubitMap().get(q)); - if (op !== undefined && ['CX', 'XCZ', 'CY', 'XCY', 'YCX', 'YCZ', 'CXSWAP', 'SWAPCX'].indexOf(op.gate.name) !== -1) { + if (op !== undefined && rev.has(op.gate.name)) { flipped_op_first_targets.add(op.id_targets[0]); } } for (let q of flipped_op_first_targets) { - layer.id_ops.get(q).id_targets.reverse(); + let op = layer.id_ops.get(q); + let other = rev.get(op.gate.name); + if (other === 'reverse') { + layer.id_ops.get(q).id_targets.reverse(); + } else { + op.gate = GATE_MAP.get(other); + } } editorState.commit_or_preview(newCircuit, preview); }); - res.set('shift+f', preview => { + res.set('g', preview => { let newCircuit = editorState.copyOfCurCircuit(); let end = editorState.curLayer; while (end < newCircuit.layers.length && !newCircuit.layers[end].empty()) { @@ -271,15 +307,15 @@ function makeChordHandlers() { } editorState.commit_or_preview(newCircuit, preview); }); - res.set('shift+>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview)); - res.set('shift+<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview)); - res.set('shift+v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview)); - res.set('shift+^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview)); - res.set('>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview)); - res.set('<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview)); - res.set('v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview)); - res.set('^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview)); - res.set('.', preview => editorState.applyCoordinateTransform((x, y) => [x + 0.5, y + 0.5], preview)); + res.set('shift+>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); + res.set('shift+<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); + res.set('shift+v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); + res.set('shift+^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); + res.set('>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); + res.set('<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); + res.set('v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); + res.set('^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); + res.set('.', preview => editorState.applyCoordinateTransform((x, y) => [x + 0.5, y + 0.5], preview, false)); /** * @param {!Array} chords @@ -436,12 +472,12 @@ function handleKeyboardEvent(ev) { editorState.chorder.handleKeyEvent(ev); if (ev.type === 'keydown') { if (ev.key.toLowerCase() === 'q') { - let d = ev.shiftKey ? 10 : 1; + let d = ev.shiftKey ? 5 : 1; editorState.changeCurLayerTo(editorState.curLayer - d); return; } if (ev.key.toLowerCase() === 'e') { - let d = ev.shiftKey ? 10 : 1; + let d = ev.shiftKey ? 5 : 1; editorState.changeCurLayerTo(editorState.curLayer + d); return; } @@ -514,3 +550,17 @@ window.addEventListener('focus', () => { window.addEventListener('blur', () => { editorState.chorder.handleFocusChanged(); }); + +// Intercept clicks on the example circuit links, and load them without actually reloading the page, to preserve undo history. +for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) { + anchor.onclick = ev => { + // Don't stop the user from e.g. opening the example in a new tab using ctrl+click. + if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) { + return undefined; + } + let circuitText = anchor.href.split('#circuit=')[1]; + + editorState.rev.commit(circuitText); + return false; + }; +}