-
Notifications
You must be signed in to change notification settings - Fork 208
Add python widget for Bloch Sphere #3595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
982173e
0b54a59
da57ee9
8aaf618
d698770
45e5579
69bb411
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /* Copyright (c) Microsoft Corporation. | ||
| Licensed under the MIT License. */ | ||
|
|
||
| /* | ||
| * The Bloch sphere renders matrices that require KaTeX's fonts for correct | ||
| * spacing and scalable delimiters. Keep these fonts out of the shared widget | ||
| * stylesheet so widgets that predate the Bloch sphere retain their original | ||
| * CSS payload. | ||
| */ | ||
| @import "../../npm/qsharp/ux/qsharp-ux.css"; | ||
| @import "../../npm/qsharp/ux/bloch/bloch.css"; | ||
| @import "./widgets.css"; | ||
| @import "katex/dist/katex.min.css"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import { | |
| MoleculeViewer, | ||
| Entanglement, | ||
| type EntanglementProps, | ||
| BlochSphere, | ||
| } from "qsharp-lang/ux"; | ||
| import markdownIt from "markdown-it"; | ||
| import "./widgets.css"; | ||
|
|
@@ -91,6 +92,9 @@ function render({ model, el }: RenderArgs) { | |
| case "Entanglement": | ||
| renderEntanglement({ model, el }); | ||
| break; | ||
| case "BlochSphere": | ||
| renderBlochSphere({ model, el }); | ||
| break; | ||
| default: | ||
| throw new Error(`Unknown component type ${componentType}`); | ||
| } | ||
|
|
@@ -289,6 +293,25 @@ function renderCircuit({ model, el }: RenderArgs) { | |
| model.on("change:circuit_json", onChange); | ||
| } | ||
|
|
||
| function renderBlochSphere({ model, el }: RenderArgs) { | ||
| const onChange = () => { | ||
| const initialGates = model.get("initial_gates") as string; | ||
| prender( | ||
| <BlochSphere | ||
| initialGates={initialGates} | ||
| onGatesChanged={(gates) => { | ||
| model.set("gates", gates); | ||
| model.save_changes(); | ||
| }} | ||
| ></BlochSphere>, | ||
| el, | ||
| ); | ||
| }; | ||
|
|
||
| onChange(); | ||
| model.on("change:initial_gates", onChange); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This listener rerenders the wrapper when Python changes I reproduced this with the wheel built from this PR: from IPython.display import display
from qsharp_widgets import BlochSphere
widget = BlochSphere("X")
display(widget)The widget's gate-program text field initially shows widget.initial_gates = "H"
print("initial_gates:", widget.initial_gates)
print("gates:", widget.gates)Observed: The rendered gate-program field also remains Could the core component respond to subsequent
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not really sure what to do about this. The initial_gates are meant to be a memory of how the bloch sphere was initialized, so maybe it should be a read-only field. But not sure what the behavior of the I also don't know what the desired behavior would be if we have multiple instances of a bloch object rendered. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Me neither :). Probably something to discuss with the team? |
||
| } | ||
|
|
||
| function renderAtoms({ model, el }: RenderArgs) { | ||
| const onChange = () => { | ||
| const machineLayout = model.get("machine_layout") as ZoneLayout; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { readFile, writeFile } from "node:fs/promises"; | ||
|
|
||
| const cssPath = new URL( | ||
| "../src/qsharp_widgets/static/bloch.css", | ||
| import.meta.url, | ||
| ); | ||
|
|
||
| // Chromium loads only these faces for the complete set of expressions emitted | ||
| // by the Bloch sphere. Keep this list descriptor-specific: KaTeX uses the same | ||
| // family name for multiple styles and weights. | ||
| const requiredFaces = new Set([ | ||
| "KaTeX_Main|normal|400", | ||
| "KaTeX_Math|italic|400", | ||
| "KaTeX_Size3|normal|400", | ||
| "KaTeX_Size4|normal|400", | ||
| ]); | ||
|
|
||
| const css = await readFile(cssPath, "utf8"); | ||
| const fontFacePattern = /@font-face\{[^}]*\}/g; | ||
| const foundRequiredFaces = new Set(); | ||
| let totalFaces = 0; | ||
| let removedFaces = 0; | ||
|
|
||
| const subsetCss = css.replace(fontFacePattern, (fontFace) => { | ||
| totalFaces += 1; | ||
|
|
||
| const family = readDescriptor(fontFace, "font-family"); | ||
| const style = readDescriptor(fontFace, "font-style") ?? "normal"; | ||
| const weight = readDescriptor(fontFace, "font-weight") ?? "400"; | ||
| const key = `${family}|${style}|${weight}`; | ||
|
|
||
| if (requiredFaces.has(key)) { | ||
| foundRequiredFaces.add(key); | ||
| return fontFace; | ||
| } | ||
|
|
||
| removedFaces += 1; | ||
| return ""; | ||
| }); | ||
|
|
||
| const missingFaces = [...requiredFaces].filter( | ||
| (face) => !foundRequiredFaces.has(face), | ||
| ); | ||
| if (missingFaces.length > 0) { | ||
| throw new Error( | ||
| `KaTeX CSS is missing required Bloch font faces: ${missingFaces.join(", ")}`, | ||
| ); | ||
| } | ||
| if (totalFaces === 0 || removedFaces === 0) { | ||
| throw new Error( | ||
| `Expected bundled KaTeX font faces to subset; found ${totalFaces}, removed ${removedFaces}`, | ||
| ); | ||
| } | ||
|
|
||
| await writeFile(cssPath, subsetCss); | ||
| console.log( | ||
| `Subset KaTeX fonts in bloch.css: kept ${foundRequiredFaces.size}, removed ${removedFaces}`, | ||
| ); | ||
|
|
||
| function readDescriptor(fontFace, descriptor) { | ||
| const match = fontFace.match(new RegExp(`${descriptor}:([^;}]+)`)); | ||
| return match?.[1]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| { | ||
| "scripts": { | ||
| "dev": "npm run build -- --sourcemap=inline --watch", | ||
| "build": "npx esbuild js/index.tsx --minify --format=esm --bundle --outdir=src/qsharp_widgets/static" | ||
| "dev": "npm run build:bundle -- --sourcemap=inline --watch", | ||
| "build": "npm run build:bundle && node js/subset-katex-fonts.mjs", | ||
| "build:bundle": "npx esbuild js/index.tsx js/bloch.css --minify --format=esm --bundle --outdir=src/qsharp_widgets/static --loader:.woff2=dataurl --loader:.woff=empty --loader:.ttf=empty" | ||
| }, | ||
| "devDependencies": {} | ||
| } |

Uh oh!
There was an error while loading. Please reload this page.