microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
jan

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/ux/circuit.tsx

370lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import * as qviz from "./circuit-vis/index.js";
5import { useEffect, useRef, useState } from "preact/hooks";
6import { CircuitProps } from "./data.js";
7import { Spinner } from "./spinner.js";
8import { SourceLocation, toCircuitGroup } from "./circuit-vis/circuit.js";
9
10// For perf reasons we set a limit on how many gates/qubits
11// we attempt to render. This is still a lot higher than a human would
12// reasonably want to look at, but it takes about a second to
13// render a circuit this big on a mid-grade laptop so we allow it.
14const MAX_OPERATIONS = 10000;
15const MAX_QUBITS = 1000;
16
17// For now we only support one circuit at a time.
18const MAX_CIRCUITS = 1;
19
20// This component is shared by the Python widget and the VS Code panel
21export function Circuit(props: {
22 circuit?: qviz.CircuitGroup | qviz.Circuit;
23 isEditable: boolean;
24 editCallback?: (fileData: qviz.CircuitGroup) => void;
25 runCallback?: () => void;
26 renderLocations: (s: SourceLocation[]) => { title: string; href: string };
27}) {
28 let unrenderable = false;
29 let qubits = 0;
30 let operations = 0;
31 let errorMsg: string | undefined = undefined;
32
33 const result = toCircuitGroup(props.circuit);
34 if (result.ok) {
35 const circuit = result.circuitGroup.circuits[0];
36 if (circuit.componentGrid === undefined) circuit.componentGrid = [];
37 if (circuit.qubits === undefined) circuit.qubits = [];
38 qubits = circuit.qubits.length;
39 operations = circuit.componentGrid.length;
40
41 unrenderable =
42 unrenderable ||
43 result.circuitGroup.circuits.length > MAX_CIRCUITS ||
44 (!props.isEditable && qubits === 0) ||
45 operations > MAX_OPERATIONS ||
46 qubits > MAX_QUBITS;
47 } else {
48 errorMsg = result.error;
49 }
50
51 return (
52 <div>
53 {!result.ok || unrenderable ? (
54 <Unrenderable
55 qubits={qubits}
56 operations={operations}
57 error={errorMsg}
58 />
59 ) : (
60 <ZoomableCircuit {...props} circuitGroup={result.circuitGroup} />
61 )}
62 </div>
63 );
64}
65
66function ZoomableCircuit(props: {
67 circuitGroup: qviz.CircuitGroup;
68 isEditable: boolean;
69 editCallback?: (fileData: qviz.CircuitGroup) => void;
70 runCallback?: () => void;
71 renderLocations: (s: SourceLocation[]) => { title: string; href: string };
72}) {
73 const circuitDiv = useRef<HTMLDivElement>(null);
74 const [zoomLevel, setZoomLevel] = useState(100);
75 const [rendering, setRendering] = useState(true);
76 const [zoomOnResize, setZoomOnResize] = useState(true);
77
78 useEffect(() => {
79 // Enable "rendering" text while the circuit is being drawn
80 setRendering(true);
81 const container = circuitDiv.current!;
82 container.innerHTML = "";
83 }, [props.circuitGroup]);
84
85 useEffect(() => {
86 if (rendering) {
87 const container = circuitDiv.current!;
88 // Draw the circuits - may take a while for large circuits
89 const svg = renderCircuits(
90 props.circuitGroup,
91 container,
92 props.isEditable,
93 props.renderLocations,
94 props.editCallback,
95 props.runCallback,
96 );
97
98 if (!props.isEditable) {
99 const initialZoom = calculateZoomToFit(container, svg as SVGElement);
100 // Set the initial zoom level
101 setZoomLevel(initialZoom);
102 // Resize the SVG to fit
103 updateWidth();
104 }
105
106 // Calculate the initial zoom level based on the container width
107 // Disable "rendering" text
108 setRendering(false);
109 } else if (!props.isEditable) {
110 // Initial drawing done, attach window resize handler
111 window.addEventListener("resize", onResize);
112 return () => {
113 window.removeEventListener("resize", onResize);
114 };
115 }
116 }, [rendering, zoomOnResize]);
117
118 useEffect(() => {
119 updateWidth();
120 }, [zoomLevel]);
121
122 return (
123 <div>
124 <div>
125 {props.isEditable || rendering ? null : (
126 <ZoomControl zoom={zoomLevel} onInput={userSetZoomLevel} />
127 )}
128 </div>
129 <div>
130 {rendering
131 ? `Rendering diagram with ${props.circuitGroup.circuits[0].componentGrid.length} gates...`
132 : ""}
133 </div>
134 <div class="qs-circuit" ref={circuitDiv}></div>
135 </div>
136 );
137
138 /**
139 * Window resize handler to recalculate and set the zoom level
140 * based on the new window width.
141 */
142 function onResize() {
143 if (!zoomOnResize) {
144 return;
145 }
146
147 const [container, svg] = [circuitDiv.current, currentSvg()];
148 if (container && svg) {
149 // Recalculate the zoom level based on the container width
150 const initialZoom = calculateZoomToFit(container, svg);
151 // Set the zoom level
152 setZoomLevel(initialZoom);
153 }
154 }
155
156 /**
157 * Update the width of the SVG element based on the current zoom level.
158 */
159 function updateWidth() {
160 const svg = currentSvg();
161 if (svg) {
162 // The width attribute contains the true width, generated by qviz.
163 // We'll leave this attribute untouched, so we can use it again if the
164 // zoom level is ever updated.
165 const width = svg.getAttribute("width")!;
166
167 // We'll set the width in the style attribute to (true width * zoom level).
168 // This value takes precedence over the true width in the width attribute.
169 svg.setAttribute(
170 "style",
171 `max-width: ${width}; width: ${(parseInt(width) * (zoomLevel || 100)) / 100}; height: auto`,
172 );
173 }
174 }
175
176 function renderCircuits(
177 circuitGroup: qviz.CircuitGroup,
178 container: HTMLDivElement,
179 isEditable: boolean,
180 renderLocations?: (s: SourceLocation[]) => { title: string; href: string },
181 editCallback?: (fileData: qviz.CircuitGroup) => void,
182 runCallback?: () => void,
183 ) {
184 qviz.draw(circuitGroup, container, {
185 isEditable,
186 editCallback,
187 runCallback,
188 renderLocations,
189 });
190 return container.getElementsByClassName("qviz")[0]!;
191 }
192
193 /**
194 * Calculate the zoom level that will fit the circuit into the current size of the container.
195 */
196 function calculateZoomToFit(container: HTMLDivElement, svg: SVGElement) {
197 const containerWidth = container.clientWidth;
198 // width and height are the true dimensions generated by qviz
199 const width = parseInt(svg.getAttribute("width")!);
200 const height = svg.getAttribute("height")!;
201
202 svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
203 const zoom = Math.min(Math.ceil((containerWidth / width) * 100), 100);
204 return zoom;
205 }
206
207 function currentSvg(): SVGElement | undefined {
208 return circuitDiv.current?.querySelector(".qviz") ?? undefined;
209 }
210
211 function userSetZoomLevel(zoom: number) {
212 setZoomOnResize(false);
213 setZoomLevel(zoom);
214 }
215}
216
217function Unrenderable(props: {
218 qubits: number;
219 operations: number;
220 error?: string;
221}) {
222 let errorDiv = null;
223
224 if (props.error) {
225 errorDiv = (
226 <div>
227 <p>
228 <b>Unable to render circuit:</b>
229 </p>
230 <pre>{props.error}</pre>
231 </div>
232 );
233 } else if (props.qubits === 0) {
234 errorDiv = (
235 <div>
236 <p>No circuit to display. No qubits have been allocated.</p>
237 </div>
238 );
239 } else if (props.operations > MAX_OPERATIONS) {
240 // Don't show the real number of operations here, as that number is
241 // *already* truncated by the underlying circuit builder.
242 errorDiv = (
243 <div>
244 <p>
245 This circuit has too many gates to display. The maximum supported
246 number of gates is {MAX_OPERATIONS}.
247 </p>
248 </div>
249 );
250 } else if (props.qubits > MAX_QUBITS) {
251 errorDiv = (
252 <div>
253 <p>
254 This circuit has too many qubits to display. It has {props.qubits}{" "}
255 qubits, but the maximum supported is {MAX_QUBITS}.
256 </p>
257 </div>
258 );
259 }
260
261 return <div class="qs-circuit-error">{errorDiv}</div>;
262}
263
264function ZoomControl(props: { zoom: number; onInput: (zoom: number) => void }) {
265 return (
266 <p>
267 <label htmlFor="qs-circuit-zoom">Zoom </label>
268 <input
269 id="qs-circuit-zoom"
270 type="number"
271 min="10"
272 max="100"
273 step="10"
274 value={props.zoom}
275 onInput={(e) =>
276 props.onInput(parseInt((e.target as HTMLInputElement).value) || 0)
277 }
278 />
279 %
280 </p>
281 );
282}
283
284// This component is exclusive to the VS Code panel
285export function CircuitPanel(props: CircuitProps) {
286 const error = props.errorHtml ? (
287 <div>
288 <p>
289 {props.circuit
290 ? "The program encountered a failure. See the error(s) below."
291 : "A circuit could not be generated for this program. See the error(s) below."}
292 <br />
293 </p>
294 <div dangerouslySetInnerHTML={{ __html: props.errorHtml }}></div>
295 </div>
296 ) : null;
297
298 return (
299 <div class="qs-circuit-panel">
300 <div>
301 <h1>
302 {props.title} {props.simulated ? "(Trace)" : ""}
303 </h1>
304 </div>
305 {error && <div class="qs-circuit-error">{error}</div>}
306 {props.targetProfile && <p>{props.targetProfile}</p>}
307 {props.simulated && (
308 <p>
309 WARNING: This diagram shows the result of tracing a dynamic circuit,
310 and may change from run to run.
311 </p>
312 )}
313 <p>
314 Learn more at{" "}
315 {props.isEditable ? (
316 <a href="https://aka.ms/qdk.circuit-editor">
317 https://aka.ms/qdk.circuit-editor
318 </a>
319 ) : (
320 <a href="https://aka.ms/qdk.circuits">https://aka.ms/qdk.circuits</a>
321 )}
322 </p>
323 {props.calculating ? (
324 <div>
325 <Spinner />
326 </div>
327 ) : null}
328 {props.circuit ? (
329 <Circuit
330 circuit={props.circuit}
331 isEditable={props.isEditable}
332 editCallback={props.editCallback}
333 runCallback={props.runCallback}
334 renderLocations={renderLocations}
335 ></Circuit>
336 ) : null}
337 </div>
338 );
339}
340
341function renderLocations(locations: SourceLocation[]) {
342 const qdkLocations = locations.map((location) => {
343 const position = {
344 line: location.line,
345 character: location.column,
346 };
347 return {
348 source: location.file,
349 span: {
350 start: position,
351 end: position,
352 },
353 };
354 });
355
356 const titles = locations.map((location) => {
357 const basename =
358 location.file.replace(/\/+$/, "").split("/").pop() ?? location.file;
359 const title = `${basename}:${location.line + 1}:${location.column + 1}`;
360 return title;
361 });
362 const title = titles.length > 1 ? `${titles[0]}, ...` : titles[0];
363
364 const argsStr = encodeURIComponent(JSON.stringify([qdkLocations]));
365 const href = `command:qsharp-vscode.gotoLocations?${argsStr}`;
366 return {
367 title,
368 href,
369 };
370}
371