Download tools/dashboard_core/circuit_builder_component.py from Tatopenn/dense-Evolution: direct link, hf CLI and curl.
- Browser
- Download file 9.32 kB
-
https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tools/dashboard_core/circuit_builder_component.py
- Command line
-
hf download hf://Tatopenn/dense-Evolution/tools/dashboard_core/circuit_builder_component.py
-
curl -L -o circuit_builder_component.py https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tools/dashboard_core/circuit_builder_component.py
9.32 kB
| """ | |
| Graphical (drag-and-drop) circuit builder -- the fourth pillar of IBM | |
| Quantum Composer's layout (graphical editor + code editor + Statevector + | |
| Probabilities), the one missing piece the rest of dashboard_core didn't | |
| have yet. | |
| Built on Streamlit's own native bidirectional component API | |
| (st.components.v2.component -- real HTML/CSS/JS running in the browser, | |
| not a hand-waved "coming soon" panel). Gates are dragged from a palette | |
| onto a qubit x time-step grid using plain HTML5 drag-and-drop; every drop | |
| re-serializes the grid into an ops list and pushes it back to Python via | |
| setStateValue, so Streamlit always has the real current state of what's | |
| on the canvas. | |
| The ops list this emits is consumed by dashboard_core.graphical_builder. | |
| ops_to_native_tuples, which builds dense_evolution's own gate tuples | |
| (no Qiskit) executed by the exact same dense_evolution engine as typed | |
| OpenQASM -- no separate/fake execution path for graphically-built | |
| circuits. | |
| """ | |
| from .graphical_builder import GATE_PALETTE | |
| __all__ = ['mount_circuit_builder'] | |
| _CSS = """ | |
| .cb-root { display: flex; flex-direction: column; gap: 10px; font-family: monospace; } | |
| .cb-palette { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px; border: 1px solid var(--st-border-color, #444); border-radius: 6px; } | |
| .cb-chip { | |
| padding: 4px 10px; border-radius: 4px; cursor: grab; user-select: none; | |
| background: var(--st-secondary-background-color, #262730); | |
| color: var(--st-text-color, #fafafa); | |
| border: 1px solid var(--st-border-color, #444); | |
| font-size: 14px; | |
| } | |
| .cb-chip:active { cursor: grabbing; } | |
| .cb-chip[data-kind="control"] { background: #3a5; color: #fff; } | |
| .cb-chip[data-kind="target"] { background: #35a; color: #fff; } | |
| .cb-chip[data-kind="swap"] { background: #a53; color: #fff; } | |
| .cb-toolbar { display: flex; gap: 8px; align-items: center; } | |
| .cb-toolbar button { | |
| padding: 4px 10px; border-radius: 4px; cursor: pointer; | |
| background: var(--st-secondary-background-color, #262730); | |
| color: var(--st-text-color, #fafafa); | |
| border: 1px solid var(--st-border-color, #444); | |
| } | |
| .cb-grid { display: grid; gap: 2px; align-items: center; position: relative; } | |
| .cb-qlabel { font-size: 12px; opacity: 0.75; text-align: right; padding-right: 6px; } | |
| .cb-cell { | |
| width: 40px; height: 32px; border: 1px solid var(--st-border-color, #444); | |
| border-radius: 4px; display: flex; align-items: center; justify-content: center; | |
| font-size: 13px; cursor: pointer; | |
| background: var(--st-background-color, #0e1117); | |
| color: var(--st-text-color, #fafafa); | |
| } | |
| .cb-cell.cb-filled { background: var(--st-secondary-background-color, #262730); font-weight: bold; } | |
| .cb-cell.cb-over { outline: 2px dashed #4af; } | |
| .cb-count { font-size: 12px; opacity: 0.75; } | |
| """ | |
| _HTML = """ | |
| <div class="cb-toolbar"> | |
| <span class="cb-count" data-role="count">0 porte piazzate</span> | |
| <button data-role="clear">Pulisci griglia</button> | |
| </div> | |
| """ | |
| _JS = r""" | |
| export default function(component) { | |
| const { data, setStateValue, parentElement } = component; | |
| const nQubits = Math.max(1, data.n_qubits || 1); | |
| const nCols = Math.max(1, data.n_columns || 12); | |
| const palette = data.palette || []; | |
| const grid = Array.from({ length: nQubits }, () => Array(nCols).fill(null)); | |
| const cellEls = Array.from({ length: nQubits }, () => Array(nCols).fill(null)); | |
| const root = document.createElement('div'); | |
| root.className = 'cb-root'; | |
| const paletteEl = document.createElement('div'); | |
| paletteEl.className = 'cb-palette'; | |
| palette.forEach((g) => { | |
| const chip = document.createElement('div'); | |
| chip.className = 'cb-chip'; | |
| chip.draggable = true; | |
| chip.textContent = g.label; | |
| chip.dataset.kind = g.kind; | |
| chip.title = g.kind === 'single' ? `Porta a 1 qubit: ${g.label}` | |
| : g.kind === 'control' ? 'Controllo -- trascina un target nella stessa colonna' | |
| : g.kind === 'target' ? `Target -- forma C${g.gate.toUpperCase()} con un controllo nella stessa colonna` | |
| : 'SWAP -- trascina due marcatori nella stessa colonna'; | |
| chip.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', g.id); | |
| e.dataTransfer.effectAllowed = 'copy'; | |
| }); | |
| paletteEl.appendChild(chip); | |
| }); | |
| root.appendChild(paletteEl); | |
| const gridEl = document.createElement('div'); | |
| gridEl.className = 'cb-grid'; | |
| gridEl.style.gridTemplateColumns = `36px repeat(${nCols}, 40px)`; | |
| for (let r = 0; r < nQubits; r++) { | |
| const label = document.createElement('div'); | |
| label.className = 'cb-qlabel'; | |
| label.textContent = `q${r}`; | |
| gridEl.appendChild(label); | |
| for (let c = 0; c < nCols; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cb-cell'; | |
| cell.addEventListener('dragover', (e) => { e.preventDefault(); cell.classList.add('cb-over'); }); | |
| cell.addEventListener('dragleave', () => cell.classList.remove('cb-over')); | |
| cell.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| cell.classList.remove('cb-over'); | |
| const gateId = e.dataTransfer.getData('text/plain'); | |
| const g = palette.find((x) => x.id === gateId); | |
| if (!g) return; | |
| grid[r][c] = { kind: g.kind, gate: g.gate }; | |
| renderCell(r, c); | |
| emit(); | |
| }); | |
| cell.addEventListener('click', () => { | |
| if (grid[r][c]) { | |
| grid[r][c] = null; | |
| renderCell(r, c); | |
| emit(); | |
| } | |
| }); | |
| gridEl.appendChild(cell); | |
| cellEls[r][c] = cell; | |
| } | |
| } | |
| root.appendChild(gridEl); | |
| parentElement.appendChild(root); | |
| const countEl = root.querySelector('[data-role="count"]'); | |
| root.querySelector('[data-role="clear"]').addEventListener('click', () => { | |
| for (let r = 0; r < nQubits; r++) { | |
| for (let c = 0; c < nCols; c++) { | |
| grid[r][c] = null; | |
| renderCell(r, c); | |
| } | |
| } | |
| emit(); | |
| }); | |
| const SYMS = { control: '●', swap: '×' }; | |
| function renderCell(r, c) { | |
| const cell = cellEls[r][c]; | |
| const v = grid[r][c]; | |
| if (!v) { | |
| cell.textContent = ''; | |
| cell.classList.remove('cb-filled'); | |
| return; | |
| } | |
| cell.classList.add('cb-filled'); | |
| if (v.kind === 'single') cell.textContent = v.gate.toUpperCase(); | |
| else if (v.kind === 'target') cell.textContent = v.gate === 'x' ? '⊕' : v.gate.toUpperCase(); | |
| else cell.textContent = SYMS[v.kind] || '?'; | |
| } | |
| function buildOps() { | |
| const ops = []; | |
| for (let c = 0; c < nCols; c++) { | |
| let single = null; | |
| const controls = []; | |
| const targets = []; | |
| const swaps = []; | |
| for (let r = 0; r < nQubits; r++) { | |
| const v = grid[r][c]; | |
| if (!v) continue; | |
| if (v.kind === 'single') single = { row: r, gate: v.gate }; | |
| else if (v.kind === 'control') controls.push(r); | |
| else if (v.kind === 'target') targets.push({ row: r, gate: v.gate }); | |
| else if (v.kind === 'swap') swaps.push(r); | |
| } | |
| const isEmpty = !single && controls.length === 0 && targets.length === 0 && swaps.length === 0; | |
| if (isEmpty) continue; | |
| if (single && controls.length === 0 && targets.length === 0 && swaps.length === 0) { | |
| ops.push({ gate: single.gate, qubits: [single.row] }); | |
| } else if (!single && controls.length === 1 && targets.length === 1 && swaps.length === 0) { | |
| ops.push({ gate: 'c' + targets[0].gate, qubits: [controls[0], targets[0].row] }); | |
| } else if (!single && controls.length === 0 && targets.length === 0 && swaps.length === 2) { | |
| ops.push({ gate: 'swap', qubits: swaps }); | |
| } | |
| // Any other combination in a column (e.g. a lone control with no | |
| // target yet) is an incomplete gate -- shown on the grid but not | |
| // emitted as an op until it's completed. | |
| } | |
| return ops; | |
| } | |
| function emit() { | |
| const ops = buildOps(); | |
| countEl.textContent = `${ops.length} porte piazzate`; | |
| setStateValue('circuit', ops); | |
| } | |
| emit(); | |
| } | |
| """ | |
| _COMPONENT_NAME = "dense_evolution_circuit_builder" | |
| def mount_circuit_builder(n_qubits: int, n_columns: int = 12, key: str = "circuit_builder"): | |
| """Mount the drag-and-drop circuit builder and return its current ops | |
| list (list[dict] with 'gate'/'qubits'), or [] if nothing is placed yet. | |
| `key` should change whenever `n_qubits` changes (e.g. include it in the | |
| caller's key) so Streamlit mounts a fresh grid instead of reusing a | |
| stale one sized for a different qubit count. | |
| Streamlit is imported here, not at module level: this component only | |
| means anything inside a Streamlit app (app_dashboard.py) -- the | |
| Composer kernel (local_site/app/server.py) imports dashboard_core too | |
| but never calls this function, and shouldn't need streamlit installed | |
| just to start. | |
| """ | |
| import streamlit as st | |
| builder = st.components.v2.component(_COMPONENT_NAME, html=_HTML, css=_CSS, js=_JS) | |
| result = builder( | |
| key=key, | |
| data={"n_qubits": n_qubits, "n_columns": n_columns, "palette": GATE_PALETTE}, | |
| default={"circuit": []}, | |
| on_circuit_change=lambda: None, | |
| ) | |
| return result.circuit if result.circuit is not None else [] | |