microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
source/playground/src/results.tsx
260lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | import { QscEventTarget, ShotResult, VSDiagnostic } from "qsharp-lang"; |
| 5 | import { useEffect, useState } from "preact/hooks"; |
| 6 | |
| 7 | import { Histogram, Markdown } from "qsharp-lang/ux"; |
| 8 | import { StateTable } from "./state.js"; |
| 9 | import { ActiveTab } from "./main.js"; |
| 10 | |
| 11 | function resultToLabel(result: string | VSDiagnostic): string { |
| 12 | if (typeof result !== "string") return "ERROR"; |
| 13 | return result; |
| 14 | } |
| 15 | |
| 16 | type ResultsState = { |
| 17 | shotCount: number; // How many shots have started |
| 18 | resultCount: number; // How many have completed (may be one less than above) |
| 19 | currIndex: number; // Which is currently being displayed |
| 20 | currResult: ShotResult | undefined; // The shot data to display |
| 21 | buckets: Map<string, number>; // Histogram buckets |
| 22 | filterValue: string; // Any filter that is in effect (or "") |
| 23 | filterIndex: number; // The index into the filtered set |
| 24 | currArray: ShotResult[]; // Used to detect a new run |
| 25 | }; |
| 26 | |
| 27 | function newRunState() { |
| 28 | return { |
| 29 | shotCount: 0, |
| 30 | resultCount: 0, |
| 31 | currIndex: 0, |
| 32 | currResult: undefined, |
| 33 | buckets: new Map(), |
| 34 | filterValue: "", |
| 35 | filterIndex: 0, |
| 36 | currArray: [], |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | function resultIsSame(a: ShotResult, b: ShotResult): boolean { |
| 41 | // If the length changed, any entries are different objects, or the final result has changed. |
| 42 | if ( |
| 43 | a.success !== b.success || |
| 44 | a.result !== b.result || |
| 45 | a.events.length !== b.events.length |
| 46 | ) |
| 47 | return false; |
| 48 | |
| 49 | for (let i = 0; i < a.events.length; ++i) { |
| 50 | if (a.events[i] !== b.events[i]) return false; |
| 51 | } |
| 52 | |
| 53 | return true; |
| 54 | } |
| 55 | |
| 56 | export function ResultsTab(props: { |
| 57 | evtTarget: QscEventTarget; |
| 58 | onShotError?: (err?: VSDiagnostic) => void; |
| 59 | kataMode?: boolean; |
| 60 | activeTab: ActiveTab; |
| 61 | }) { |
| 62 | const [resultState, setResultState] = useState<ResultsState>(newRunState()); |
| 63 | |
| 64 | // This is more complex than ideal for performance reasons. During a run, results may be getting |
| 65 | // updated thousands of times a second, but there is no point trying to render at more than 60fps. |
| 66 | // Therefore this subscribes to an event that happens once a frame if changes to results occur. |
| 67 | // As the results are mutated array, they don't make good props or state, so need to manually |
| 68 | // check for changes that would impact rendering and update state by creating new objects. |
| 69 | const evtTarget = props.evtTarget; |
| 70 | useEffect(() => { |
| 71 | const resultUpdateHandler = () => { |
| 72 | const results = evtTarget.getResults(); |
| 73 | |
| 74 | // If it's a new run, the entire results array will be a new object |
| 75 | const isNewResults = results !== resultState.currArray; |
| 76 | |
| 77 | // If the results object has changed then reset the current index and filter. |
| 78 | const newIndex = isNewResults ? 0 : resultState.currIndex; |
| 79 | const newFilterValue = isNewResults ? "" : resultState.filterValue; |
| 80 | const newFilterIndex = isNewResults ? 0 : resultState.filterIndex; |
| 81 | |
| 82 | const currentResult = resultState.currResult; |
| 83 | const updatedResult = |
| 84 | newIndex < results.length ? results[newIndex] : undefined; |
| 85 | |
| 86 | const replaceResult = |
| 87 | isNewResults || |
| 88 | // One is defined but the other isn't |
| 89 | !currentResult !== !updatedResult || |
| 90 | // Or they both exist but are different (e.g. may have new events of have completed) |
| 91 | (currentResult && |
| 92 | updatedResult && |
| 93 | !resultIsSame(currentResult, updatedResult)); |
| 94 | |
| 95 | // Keep the old object if no need to replace it, else construct a new one |
| 96 | const newResult = !replaceResult |
| 97 | ? currentResult |
| 98 | : !updatedResult |
| 99 | ? undefined |
| 100 | : { |
| 101 | success: updatedResult.success, |
| 102 | result: updatedResult.result, |
| 103 | events: [...updatedResult.events], |
| 104 | }; |
| 105 | |
| 106 | // Update the histogram if new results have come in. |
| 107 | // For now, just completely recreate the bucket map |
| 108 | const resultCount = evtTarget.resultCount(); |
| 109 | let buckets = resultState.buckets; |
| 110 | // If there are entirely new results, or if new results have been added, recalculate. |
| 111 | if (isNewResults || resultState.resultCount !== resultCount) { |
| 112 | buckets = new Map(); |
| 113 | for (let i = 0; i < resultCount; ++i) { |
| 114 | const key = results[i].result; |
| 115 | const strKey = resultToLabel(key); |
| 116 | const newValue = (buckets.get(strKey) || 0) + 1; |
| 117 | buckets.set(strKey, newValue); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | // If anything needs updating, construct the new state object and store |
| 122 | if ( |
| 123 | replaceResult || |
| 124 | resultState.shotCount !== results.length || |
| 125 | resultState.resultCount !== resultCount || |
| 126 | resultState.currIndex !== newIndex |
| 127 | ) { |
| 128 | setResultState({ |
| 129 | shotCount: results.length, |
| 130 | resultCount: resultCount, |
| 131 | currIndex: newIndex, |
| 132 | currResult: newResult, |
| 133 | filterValue: newFilterValue, |
| 134 | filterIndex: newFilterIndex, |
| 135 | buckets, |
| 136 | currArray: results, |
| 137 | }); |
| 138 | updateEditorError(newResult); |
| 139 | } |
| 140 | }; |
| 141 | |
| 142 | evtTarget.addEventListener("uiResultsRefresh", resultUpdateHandler); |
| 143 | |
| 144 | // Remove the event listener when this component is destroyed |
| 145 | return () => |
| 146 | evtTarget.removeEventListener("uiResultsRefresh", resultUpdateHandler); |
| 147 | }, [evtTarget]); |
| 148 | |
| 149 | // If there's a filter set, there must have been at least one item for that result. |
| 150 | // If there's no filter set, may well be no results at all yet. |
| 151 | |
| 152 | const filterValue = resultState.filterValue; |
| 153 | const countForFilter = filterValue |
| 154 | ? resultState.buckets.get(filterValue) || 0 |
| 155 | : resultState.shotCount; |
| 156 | const currIndex = filterValue |
| 157 | ? resultState.filterIndex |
| 158 | : resultState.currIndex; |
| 159 | const resultLabel = |
| 160 | typeof resultState.currResult?.result === "string" |
| 161 | ? resultToLabel(resultState.currResult?.result || "") |
| 162 | : `ERROR: ${resultState.currResult?.result.message}`; |
| 163 | |
| 164 | function moveToIndex(idx: number, filter: string) { |
| 165 | const results = evtTarget.getResults(); |
| 166 | |
| 167 | // The non-filtered default case |
| 168 | let currIndex = idx; |
| 169 | let currResult = results[idx]; |
| 170 | |
| 171 | // If a filter is in effect, need to find the filtered index |
| 172 | if (filter !== "") { |
| 173 | let found = 0; |
| 174 | for (let i = 0; i < results.length; ++i) { |
| 175 | // The buckets to filter on have been converted to kets where possible |
| 176 | if (resultToLabel(results[i].result) !== filter) continue; |
| 177 | if (found === idx) { |
| 178 | currIndex = i; |
| 179 | currResult = results[i]; |
| 180 | break; |
| 181 | } |
| 182 | ++found; |
| 183 | } |
| 184 | } |
| 185 | setResultState({ |
| 186 | ...resultState, |
| 187 | filterValue: filter, |
| 188 | filterIndex: idx, |
| 189 | currIndex, |
| 190 | currResult, |
| 191 | }); |
| 192 | updateEditorError(currResult); |
| 193 | } |
| 194 | |
| 195 | function updateEditorError(result?: ShotResult) { |
| 196 | if (!props.onShotError) return; |
| 197 | if (!result || result.success || typeof result.result === "string") { |
| 198 | props.onShotError(); |
| 199 | } else { |
| 200 | props.onShotError(result.result); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | function onPrev() { |
| 205 | if (currIndex > 0) moveToIndex(currIndex - 1, filterValue); |
| 206 | } |
| 207 | |
| 208 | function onNext() { |
| 209 | if (currIndex < countForFilter - 1) moveToIndex(currIndex + 1, filterValue); |
| 210 | } |
| 211 | |
| 212 | return props.activeTab === "results-tab" ? ( |
| 213 | <div> |
| 214 | {!resultState.shotCount ? null : ( |
| 215 | <> |
| 216 | {resultState.buckets.size > 1 ? ( |
| 217 | <Histogram |
| 218 | shotCount={resultState.shotCount} |
| 219 | data={resultState.buckets} |
| 220 | filter={filterValue} |
| 221 | onFilter={(val: string) => moveToIndex(0, val)} |
| 222 | shotsHeader={false} |
| 223 | ></Histogram> |
| 224 | ) : null} |
| 225 | {props.kataMode ? null : ( |
| 226 | <> |
| 227 | <div class="output-header"> |
| 228 | <div> |
| 229 | Shot {currIndex + 1} of {countForFilter} |
| 230 | </div> |
| 231 | <div class="prev-next"> |
| 232 | <span onClick={onPrev}>Prev</span> |{" "} |
| 233 | <span onClick={onNext}>Next</span> |
| 234 | </div> |
| 235 | </div> |
| 236 | <div class="result-label">Result: {resultLabel}</div> |
| 237 | </> |
| 238 | )} |
| 239 | <div> |
| 240 | {resultState.currResult?.events.map((evt) => { |
| 241 | return evt.type === "Message" ? ( |
| 242 | <div class="message-output">> {evt.message}</div> |
| 243 | ) : evt.type === "DumpMachine" ? ( |
| 244 | <div> |
| 245 | <StateTable |
| 246 | dump={evt.state} |
| 247 | latexDump={evt.stateLatex} |
| 248 | count={evt.qubitCount} |
| 249 | ></StateTable> |
| 250 | </div> |
| 251 | ) : ( |
| 252 | <Markdown markdown={evt.matrixLatex} /> |
| 253 | ); |
| 254 | })} |
| 255 | </div> |
| 256 | </> |
| 257 | )} |
| 258 | </div> |
| 259 | ) : null; |
| 260 | } |
| 261 | |