UNPKG

@qbead/bloch-sphere

Version:

A 3D Bloch Sphere visualisation built with Three.js and TypeScript.

1,802 lines (1,765 loc) 53.9 kB
'use strict'; var THREE = require('three'); var CSS2DRenderer_js = require('three/examples/jsm/renderers/CSS2DRenderer.js'); var Addons_js = require('three/examples/jsm/Addons.js'); var intween = require('intween'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE); new THREE.Color(1114112); const darkBlue = new THREE.Color(2567234); const lightBlue = new THREE.Color(5129949); const greyBlue = new THREE.Color(5003915); const babyBlue = new THREE.Color(4956899); const green = new THREE.Color(5617541); const lightGreen = new THREE.Color(9751106); const magenta = new THREE.Color(12792181); const yellow = new THREE.Color(14791998); const beige = new THREE.Color(14474984); const red = new THREE.Color(15744557); const defaultColors = { text: beige, background: darkBlue, blochSphereSkin: greyBlue, grid: darkBlue, axisXPlus: red, axisXMinus: babyBlue, axisYPlus: lightGreen, axisYMinus: magenta, axisZPlus: lightBlue, axisZMinus: yellow, operator: yellow, operatorPath: beige, path: beige, region: green }; const defaultText = defaultColors.text.getStyle(); var STYLES = ` <style> .bloch-sphere-widget-container { position: relative; } .label, .axis-label, .angle-label { line-height: 1; display: inline-block; color: var(--label-color, ${defaultText}); text-align: center; font-size: 1em; font-family: monospace; text-shadow: 0 0 2px black; } .axis-label { font-size: 1.6em; } .axis-label::before { content: ''; border: solid var(--label-color, ${defaultText}); border-width: 0 0 0 3px; display: inline-block; padding: 0.4em; transform: scale(0.6, 1.55) translate(0.6em, 0.036em); } .axis-label::after { content: ''; border: solid var(--label-color, ${defaultText}); border-width: 0 3px 3px 0; display: inline-block; padding: 0.4em; transform: scaleX(0.6) translate(-0.4em, 0.1em) rotate(-45deg); } </style> `; class BaseComponent extends THREE__namespace.Object3D { _color; constructor(name) { super(); this.userData.component = this; this._color = new THREE__namespace.Color(16777215); if (name) { this.name = name; } } /** * Get color of the component */ get color() { return this._color; } /** * Set color of the component */ set color(color) { this._color.set(color); this.traverse((child) => { if (child instanceof THREE__namespace.Mesh) { child.material.color.set(this._color); } }); } } class Label extends BaseComponent { htmlobj; /** * Create a new label * @param text The text to display * @param type The type of label, corresponding to the html class (default: 'label') */ constructor(text, type = "label") { super("label"); const el = document.createElement("label"); el.className = type; el.textContent = text; el.setAttribute("style", `--label-color: ${defaultColors.text.getStyle()}`); this.htmlobj = new CSS2DRenderer_js.CSS2DObject(el); this.htmlobj.position.set(0, 0, 0); this.htmlobj.userData.component = this; this.add(this.htmlobj); } get text() { return this.htmlobj.element.textContent || ""; } set text(text) { this.htmlobj.element.textContent = text; if (!text) { this.visible = false; } } get fontSize() { return parseInt(this.htmlobj.element.style.fontSize || "18"); } set fontSize(size) { this.htmlobj.element.style.fontSize = `${size}em`; } get color() { return this._color; } set color(color) { this._color.set(color); this.htmlobj.element.style.setProperty( "--label-color", `${this._color.getStyle(THREE__namespace.LinearSRGBColorSpace)}` ); } /** * Cleanup tasks */ destroy() { this.htmlobj.element.remove(); } } const BlockSphereSceneOptions = { backgroundColor: defaultColors.background, gridColor: defaultColors.grid, gridDivisions: 36 / 3, sphereSkinColor: defaultColors.blochSphereSkin, sphereSkinOpacity: 0.55 }; class BlochSphereScene extends THREE__namespace.Scene { sphere; grids; axes; labels = {}; plotStage = new THREE__namespace.Group(); constructor(options) { options = Object.assign( {}, BlockSphereSceneOptions, options ); super(); this.background = new THREE__namespace.Color(options.backgroundColor); this.fog = new THREE__namespace.Fog(options.backgroundColor, 14.5, 17); const light = new THREE__namespace.DirectionalLight(16777215, 1); light.position.set(1, 1, 1); this.add(light); this.sphere = new THREE__namespace.Group(); this.grids = new THREE__namespace.Group(); this.sphere.add(this.grids); const edges = new THREE__namespace.EdgesGeometry( new THREE__namespace.SphereGeometry(1, options.gridDivisions, options.gridDivisions), 0.5 ); const grid = new THREE__namespace.LineSegments( edges, new THREE__namespace.LineBasicMaterial({ // color: 0xdd9900, color: options.gridColor, transparent: true, opacity: 0.35, linewidth: 1 }) ); grid.rotation.x = Math.PI / 2; grid.name = "grid"; this.grids.add(grid); const polarGrid = new THREE__namespace.PolarGridHelper( 0.98, options.gridDivisions, 2, 64, options.gridColor, options.gridColor ); polarGrid.rotation.x = Math.PI / 2; polarGrid.position.z = 1e-3; polarGrid.name = "polar-grid"; this.grids.add(polarGrid); const disc = new THREE__namespace.Mesh( new THREE__namespace.CircleGeometry(0.98, 64), new THREE__namespace.MeshBasicMaterial({ color: options.sphereSkinColor, side: THREE__namespace.DoubleSide, transparent: true, opacity: options.sphereSkinOpacity }) ); this.sphere.add(disc); const sphereSkin = new THREE__namespace.Mesh( new THREE__namespace.SphereGeometry(0.995, 32, 32), new THREE__namespace.MeshBasicMaterial({ color: options.sphereSkinColor, transparent: true, opacity: options.sphereSkinOpacity, side: THREE__namespace.BackSide }) ); sphereSkin.rotation.x = Math.PI / 2; this.sphere.add(sphereSkin); this.axes = new THREE__namespace.Group(); this.sphere.add(this.axes); const axes = new THREE__namespace.AxesHelper(1.25); axes.position.set(0, 0, 1e-3); axes.setColors( defaultColors.axisXPlus, defaultColors.axisYPlus, defaultColors.axisZPlus ); axes.material.depthFunc = THREE__namespace.AlwaysDepth; this.axes.add(axes); const inverseAxes = new THREE__namespace.AxesHelper(1.25); inverseAxes.setColors( defaultColors.axisXMinus, defaultColors.axisYMinus, defaultColors.axisZMinus ); inverseAxes.position.set(0, 0, -1e-3); inverseAxes.scale.set(-1, -1, -1); inverseAxes.material.depthFunc = THREE__namespace.AlwaysDepth; this.axes.add(inverseAxes); this.sphere.add(this.plotStage); this.add(this.sphere); this.initLabels(); this.backgroundColor = options.backgroundColor; } get backgroundColor() { return this.background; } set backgroundColor(color) { this.background = new THREE__namespace.Color(color); this.fog.color = new THREE__namespace.Color(color); } initLabels() { const labels = [ { id: "zero", text: "0", position: new THREE__namespace.Vector3(0, 0, 1), // color: new THREE.Color(0x0000ff), color: new THREE__namespace.Color(defaultColors.axisZPlus), type: "axis-label" }, { id: "one", text: "1", position: new THREE__namespace.Vector3(0, 0, -1), // color: new THREE.Color(0xffff00), color: new THREE__namespace.Color(defaultColors.axisZMinus), type: "axis-label" }, { id: "plus", text: "+", position: new THREE__namespace.Vector3(1, 0, 0), // color: new THREE.Color(0xff0000), color: new THREE__namespace.Color(defaultColors.axisXPlus), type: "axis-label" }, { id: "minus", text: "-", position: new THREE__namespace.Vector3(-1, 0, 0), // color: new THREE.Color(0x00ffff), color: new THREE__namespace.Color(defaultColors.axisXMinus), type: "axis-label" }, { id: "i", text: "+i", position: new THREE__namespace.Vector3(0, 1, 0), // color: new THREE.Color(0x00ff00), color: new THREE__namespace.Color(defaultColors.axisYPlus), type: "axis-label" }, { id: "minus-i", text: "-i", position: new THREE__namespace.Vector3(0, -1, 0), // color: new THREE.Color(0xff00ff), color: new THREE__namespace.Color(defaultColors.axisYMinus), type: "axis-label" } ]; labels.forEach((label) => { const l = new Label(label.text, label.type); const color = label.color; l.position.copy(label.position).multiplyScalar(1.35); l.color = color; this.labels[label.id] = l; this.axes.add(l); }); } clearPlot() { this.plotStage.traverse((child) => { if (child instanceof Label) { child.destroy(); } }); this.plotStage.clear(); } } class Complex { real; imag; static get ZERO() { return new Complex(0, 0); } static get ONE() { return new Complex(1, 0); } static get I() { return new Complex(0, 1); } constructor(real, imag = 0) { this.real = real; this.imag = imag; } static from(value, imag) { if (typeof value === "number") { return new Complex(value, imag); } if (Array.isArray(value)) { return new Complex(value[0], value[1]); } return new Complex(value.real, value.imag); } static random() { return Complex.from(Math.random(), Math.random()); } static unitRandom() { const theta = Math.random() * 2 * Math.PI; return Complex.from(Math.cos(theta), Math.sin(theta)); } static fromPolar(magnitude, phase) { return Complex.from( magnitude * Math.cos(phase), magnitude * Math.sin(phase) ); } copy(other) { this.real = other.real; this.imag = other.imag; return this; } clone() { return new Complex(this.real, this.imag); } plus(other) { const { real, imag } = Complex.from(other); return Complex.from(this.real + real, this.imag + imag); } minus(other) { const { real, imag } = Complex.from(other); return Complex.from(this.real - real, this.imag - imag); } times(other) { const { real, imag } = Complex.from(other); return Complex.from( this.real * real - this.imag * imag, this.real * imag + this.imag * real ); } dividedBy(other) { const { real, imag } = Complex.from(other); const denominator = real * real + imag * imag; return Complex.from( (this.real * real + this.imag * imag) / denominator, (this.imag * real - this.real * imag) / denominator ); } get magnitude() { return Math.sqrt(this.real * this.real + this.imag * this.imag); } get phase() { return Math.atan2(this.imag, this.real); } conjugate() { return Complex.from(this.real, -this.imag); } reciprocal() { const denominator = this.real * this.real + this.imag * this.imag; return Complex.from(this.real / denominator, -this.imag / denominator); } pow(exponent) { const r = this.magnitude ** exponent; const theta = this.phase * exponent; return Complex.fromPolar(r, theta); } sqrt() { return this.pow(0.5); } toString() { return `${this.real} + ${this.imag}i`; } } function normalizeAzimuthal(angle) { const twoPi = 2 * Math.PI; return (angle % twoPi + twoPi) % twoPi; } function getRotationArc(v, axis, angle) { const norm = axis.clone().normalize(); const toLocal = new THREE__namespace.Quaternion().setFromUnitVectors( norm, new THREE__namespace.Vector3(0, 0, 1) ); const vLocal = v.clone().applyQuaternion(toLocal); const height = vLocal.z; const radius = Math.sqrt(vLocal.x * vLocal.x + vLocal.y * vLocal.y); if (radius === 0) { return { radius: 0, height, norm, arcOffset: 0, arcAngle: angle }; } const arcOffset = Math.atan2(vLocal.y, vLocal.x); return { radius, height, norm, arcOffset, arcAngle: angle }; } function getArcBetween(v1, v2) { const norm = v1.clone().cross(v2).normalize(); const arcAngle = v1.angleTo(v2); const rot = new THREE__namespace.Quaternion().setFromUnitVectors( new THREE__namespace.Vector3(0, 0, 1), norm ); const xaxis = new THREE__namespace.Vector3(1, 0, 0).applyQuaternion(rot); const arcOffset = xaxis.angleTo(v1) * (v1.cross(xaxis).dot(norm) < 0 ? 1 : -1); return { norm, arcOffset, arcAngle }; } function shortestModDist(a0, a1, modulo) { a0 = (a0 % modulo + modulo) % modulo; let delta = (a1 - a0) % modulo; if (delta > modulo / 2) { delta -= modulo; } else if (delta < -modulo / 2) { delta += modulo; } return delta; } function axisFromQuaternion(q) { const v = new THREE__namespace.Vector3(q.x, q.y, q.z); if (v.length() < 1e-6) { return { axis: new THREE__namespace.Vector3(0, 0, 0), angle: 0 }; } const angle = 2 * Math.atan2(v.length(), q.w); return { axis: v.normalize(), angle }; } function lerp(a, b, t) { return a + t * (b - a); } function lerpAngle(a, b, t) { const delta = shortestModDist(a, b, 2 * Math.PI); return normalizeAzimuthal(a + delta * t); } class BlochVector extends THREE.Vector3 { /** * A bloch vector representing the zero state */ static get ZERO() { return new BlochVector(0, 0, 1); } /** * A bloch vector representing the one state */ static get ONE() { return new BlochVector(0, 0, -1); } /** * A bloch vector representing the plus state (|+>) or (|0> + |1>)/sqrt(2) */ static get PLUS() { return new BlochVector(1, 0, 0); } /** * A bloch vector representing the minus state (|->) or (|0> - |1>)/sqrt(2) */ static get MINUS() { return new BlochVector(-1, 0, 0); } /** * A bloch vector representing the imaginary state (|i>) or (|0> + i|1>)/sqrt(2) */ static get I() { return new BlochVector(0, 1, 0); } /** * A bloch vector representing the minus imaginary state (|-i>) or (|0> - i|1>)/sqrt(2) */ static get MINUS_I() { return new BlochVector(0, -1, 0); } /** * Generate a random Bloch vector with magnitude 1 */ static random() { const theta = Math.random() * Math.PI; const phi = Math.random() * 2 * Math.PI; return BlochVector.fromAngles(theta, phi); } /** * Create a zero state Bloch vector */ static zero() { return BlochVector.from(BlochVector.ZERO); } static from(x, y = 0, z = 0) { if (Array.isArray(x)) { return new BlochVector(x[0], x[1], x[2]); } if (x instanceof BlochVector) { return x.clone(); } if (x instanceof THREE.Vector3) { return new BlochVector(x.x, x.y, x.z); } return new BlochVector(x, y, z); } /** * Create a Bloch vector from angles (theta, phi) */ static fromAngles(theta, phi) { return BlochVector.zero().setAngles([theta, phi]); } /** The polar angle. The angle between the BlochVector and the z-axis */ get theta() { return Math.acos(this.z); } /** The azimuthal xy-plane angle. The angle between the projection of the BlochVector on the xy-plane and the x-axis */ get phi() { return normalizeAzimuthal(Math.atan2(this.y, this.x)); } /** The amplitude of the Bloch vector */ get amplitude() { return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2); } /** The density matrix representation of the Bloch vector */ get rho() { return this.densityMatrix(); } /** The density matrix representation of the Bloch vector */ densityMatrix() { const x = Complex.from(this.x); const y = Complex.from(this.y); const z = Complex.from(this.z); const half = Complex.from(0.5); const i = Complex.from(0, 1); return [ [half.times(z.plus(1)), half.times(x.minus(i.times(y)))], [half.times(x.plus(i.times(y))), half.times(Complex.ONE.minus(z))] ]; } /** * Create a Bloch vector from a density matrix * * @param rho - The density matrix to create the Bloch vector from */ static fromDensityMatrix(rho) { const x = rho[0][1].real * 2; const y = rho[1][0].imag * 2; const z = rho[0][0].minus(rho[1][1]).real; return new BlochVector(x, y, z); } /** * Apply an operator to the Bloch vector returning a new Bloch vector * * @param op - The operator to apply * @returns The new Bloch vector */ applyOperator(op) { return BlochVector.fromDensityMatrix(op.applyTo(this.rho)); } /** * Get both angles of the Bloch vector as an array `[theta, phi]` */ angles() { return [this.theta, this.phi]; } /** * Set the Bloch vector from angles `[theta, phi]` (polar, azimuthal) * * @param angles - The angles to set the Bloch vector to */ setAngles(angles) { const [theta, phi] = angles; this.set( Math.sin(theta) * Math.cos(phi), Math.sin(theta) * Math.sin(phi), Math.cos(theta) ); return this; } toString() { return `(${this.x}, ${this.y}, ${this.z})`; } /** * Spherical linear interpolation of this Bloch vector to another Bloch vector * * @param other - The other Bloch vector to interpolate to * @param t - The interpolation factor (0 <= t <= 1) * @returns The interpolated Bloch vector */ slerpTo(other, t) { const theta = lerpAngle(this.theta, other.theta, t); const phi = lerpAngle(this.phi, other.phi, t); return BlochVector.fromAngles(theta, phi); } } function deferred() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function animate(callback, duration = 1e3, easing = "linear", loop = false) { const easeFn = intween.Parsers.parseEasing(easing); let start = performance.now(); let cancelled = false; const { promise, resolve, reject } = deferred(); const cancellablePromise = promise; const step = () => { if (cancelled) { if (!loop) { callback(1); } resolve(); return; } try { const time = performance.now(); const progress = Math.min((time - start) / duration, 1); const k = easeFn(progress); callback(k); if (progress >= 1) { if (loop) { start = time; } else { resolve(); return; } } requestAnimationFrame(step); } catch (error) { reject(error); } }; cancellablePromise.cancel = () => { if (!cancelled) { cancelled = true; } }; requestAnimationFrame(step); return cancellablePromise; } class BlochSphere { renderer; cssRenderer; el; scene; camera; controls; _cameraAnimation = null; constructor(options) { this.initRenderer(); this.camera = new THREE__namespace.OrthographicCamera(-2, 2, 2, -2, 0.1, 50); this.camera.position.set(10, 10, 5); this.camera.up.set(0, 0, 1); this.controls = new Addons_js.OrbitControls(this.camera, this.renderer.domElement); this.controls.enablePan = false; this.controls.enableZoom = true; this.controls.enableRotate = true; this.scene = new BlochSphereScene(options); this.setOptions(options); } setOptions(options) { if (!options) return; if (options.fontSize) { this.el.style.fontSize = `${options.fontSize}em`; } if (options.showGrid !== void 0) { this.showGrid = options.showGrid; } if (options.cameraState) { this.setCameraState(options.cameraState); } if (options.interactive !== void 0) { this.interactivity(options.interactive); } else { const interactivityOptions = {}; if (options.enableZoom !== void 0) { interactivityOptions.zoom = options.enableZoom; } if (options.enableRotate !== void 0) { interactivityOptions.rotate = options.enableRotate; } if (Object.keys(interactivityOptions).length > 0) { this.interactivity(interactivityOptions); } } } initRenderer() { this.el = document.createElement("div"); this.el.className = "bloch-sphere-widget-container"; this.el.innerHTML = STYLES; this.renderer = new THREE__namespace.WebGLRenderer({ antialias: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(200, 200); this.el.appendChild(this.renderer.domElement); this.cssRenderer = new CSS2DRenderer_js.CSS2DRenderer(); this.cssRenderer.setSize(200, 200); this.cssRenderer.domElement.style.position = "absolute"; this.cssRenderer.domElement.style.top = "0"; this.cssRenderer.domElement.style.pointerEvents = "none"; this.cssRenderer.domElement.style.zIndex = "1"; this.el.appendChild(this.cssRenderer.domElement); } get showGrid() { return this.scene.grids.visible; } set showGrid(value) { this.scene.grids.visible = value; } add(item) { this.scene.plotStage.add(item); } remove(item) { this.scene.plotStage.remove(item); } /** * Removes all objects from the plot * * This will not remove the grid or the sphere. */ clearPlot() { this.scene.clearPlot(); } /** * Rescales the sphere */ scale(size) { this.scene.sphere.scale.set(size, size, size); } /** * Attaches the widget to a parent element * * Must be called to make the widget visible. */ attach(parent) { parent = parent ?? document.body; parent.appendChild(this.el); this.resize(); this.start(); } /** * Resizes the widget to fit the parent element * * Optionally, you can specify the width and height to resize to. */ resize(width, height) { width = width ?? this.el.parentElement?.clientWidth ?? 200; height = height ?? this.el.parentElement?.clientHeight ?? 200; let aspect = height / width; this.renderer.setSize(width, height); this.cssRenderer.setSize(width, height); this.camera.top = 2 * aspect; this.camera.bottom = -2 * aspect; this.camera.updateProjectionMatrix(); } /** * Renders the scene * * This is called automatically in the animation loop unless that * loop is stopped. */ render() { this.renderer.render(this.scene, this.camera); this.cssRenderer.render(this.scene, this.camera); this.controls.update(); } /** * Starts the animation loop * * Automatically started when the widget is attached to a parent element. * * This will call the render method automatically. */ start() { this.renderer.setAnimationLoop(() => { this.render(); }); } /** * Stops the animation loop * * This will stop the render loop */ stop() { this.renderer.setAnimationLoop(null); } // Camera API Methods /** * Core method to set camera state with optional animation * This is the single source of truth for camera positioning - other methods delegate to this */ _setCameraState(cameraState, duration = 0, easing = "quadInOut") { if (this._cameraAnimation) { this._cameraAnimation.cancel(); this._cameraAnimation = null; } const currentAngles = this.getCameraAngles(); const currentState = { theta: currentAngles[0], phi: currentAngles[1], zoom: this.getCameraZoom() }; const targetState = { theta: cameraState.theta ?? currentState.theta, phi: cameraState.phi ?? currentState.phi, zoom: cameraState.zoom ?? currentState.zoom }; if (duration > 0) { this._cameraAnimation = animate( (progress) => { const interpolatedState = { theta: lerp(currentState.theta, targetState.theta, progress), phi: lerp(currentState.phi, targetState.phi, progress), zoom: lerp(currentState.zoom, targetState.zoom, progress) }; this._applyCameraState(interpolatedState); }, duration, easing ); this._cameraAnimation.then(() => { this._cameraAnimation = null; }); return this._cameraAnimation; } else { this._applyCameraState(targetState); } } /** * Immediately apply camera state without animation */ _applyCameraState(state) { const blochVector = BlochVector.fromAngles(state.theta, state.phi); const cameraPosition = blochVector.clone().normalize().multiplyScalar(15); this.camera.position.copy(cameraPosition); this.camera.lookAt(0, 0, 0); this.camera.zoom = state.zoom; this.camera.updateProjectionMatrix(); this.controls.update(); } /** * Get current camera zoom level */ getCameraZoom() { return this.camera.zoom; } /** * Get current camera angles as [theta, phi] */ getCameraAngles() { const blochVector = this.getCameraBlochVector(); return [blochVector.theta, blochVector.phi]; } /** * Get the Bloch vector pointing from origin to camera */ getCameraBlochVector() { const direction = this.camera.position.clone().normalize(); return new BlochVector(direction.x, direction.y, direction.z); } /** * Set camera state (unified method) */ setCameraState(cameraState, duration, easing) { return this._setCameraState(cameraState, duration, easing); } /** * Position camera such that the given Bloch vector points directly at camera */ setCameraToBlochVector(blochVector, duration, easing) { const currentZoom = this.getCameraZoom(); const cameraState = { theta: blochVector.theta, phi: blochVector.phi, zoom: currentZoom }; return this._setCameraState(cameraState, duration, easing); } /** * Set camera position using spherical coordinates */ setCameraAngles(theta, phi, duration, easing) { const currentZoom = this.getCameraZoom(); const cameraState = { theta, phi, zoom: currentZoom }; return this._setCameraState(cameraState, duration, easing); } /** * Set camera zoom level */ setCameraZoom(zoomLevel, duration, easing) { const [theta, phi] = this.getCameraAngles(); const cameraState = { theta, phi, zoom: zoomLevel }; return this._setCameraState(cameraState, duration, easing); } /** * Control user interactivity with the camera * * @param options - Interactivity options or boolean to enable/disable all interactions * @returns Current interactivity state if no arguments provided */ interactivity(options) { if (options === void 0) { return { zoom: this.controls.enableZoom, rotate: this.controls.enableRotate }; } if (typeof options === "boolean") { this.controls.enabled = options; this.controls.enableZoom = options; this.controls.enableRotate = options; } else { if (options.zoom !== void 0) { this.controls.enableZoom = options.zoom; } if (options.rotate !== void 0) { this.controls.enableRotate = options.rotate; } this.controls.enabled = this.controls.enableZoom || this.controls.enableRotate; } return { zoom: this.controls.enableZoom, rotate: this.controls.enableRotate }; } /** * Performs cleanup and disposes everything contained in the widget */ dispose() { if (this._cameraAnimation) { this._cameraAnimation.cancel(); this._cameraAnimation = null; } this.stop(); this.clearPlot(); this.renderer.dispose(); this.el.remove(); this.scene.traverse((child) => { if (child instanceof THREE__namespace.Mesh) { child.geometry.dispose(); child.material.dispose(); } }); } } function formatVector(v, precision = 2) { const xyz = [v.x, v.y, v.z].map((n) => n.toFixed(precision)); return `(${xyz.join(", ")})`; } function formatDegrees(radians, precision = 2) { return `${(radians * 180 / Math.PI).toFixed(precision)}\xB0`; } function formatRadians(radians, precision = 2) { return `${radians.toFixed(precision)} rad`; } class QubitArrow extends BaseComponent { arrowHelper; label; constructor() { super("qubit-arrow"); const arrow = new THREE__namespace.ArrowHelper( new THREE__namespace.Vector3(0, 0, 1), new THREE__namespace.Vector3(0, 0, 0), 1, 16777215, 0.1, 0.05 ); this.arrowHelper = arrow; this.label = new Label("(0, 0, 0)"); this.label.position.set(0, 1.1, 0); this.arrowHelper.add(this.label); this.arrowHelper.userData.component = this; this.add(this.arrowHelper); } set color(color) { this._color.set(color); this.arrowHelper.setColor(new THREE__namespace.Color(color)); } follow(v) { this.arrowHelper.setDirection(v); this.label.text = formatVector(v); } } function isRadiansUnits(units) { return ["rad", "RAD", "radians"].includes(units); } const yAxis = new THREE__namespace.Vector3(0, 1, 0); class AngleIndicators extends BaseComponent { units = "deg"; phiWedge; phiLabel; thetaLabelContainer; thetaWedge; thetaLabel; phiLabelContainer; _phiColor = new THREE__namespace.Color(defaultColors.text); _thetaColor = new THREE__namespace.Color(defaultColors.text); /** * Creates a new AngleIndicators component * * @param scale - The scale of the angle indicators (default is 0.25) */ constructor(scale = 0.25) { super("angle-indicators"); this.phiWedge = new THREE__namespace.Mesh( new THREE__namespace.RingGeometry(0, 1, 16, 1, 0, Math.PI), new THREE__namespace.MeshBasicMaterial({ color: this._phiColor, transparent: true, opacity: 0.35, side: THREE__namespace.DoubleSide }) ); this.phiWedge.material.depthTest = false; this.phiWedge.renderOrder = 2; this.phiLabel = new Label("0", "label angle-label"); this.phiLabel.position.set(1, 0, 0); this.phiLabelContainer = new THREE__namespace.Object3D(); this.phiLabelContainer.add(this.phiLabel); this.phiWedge.add(this.phiLabelContainer); this.thetaWedge = new THREE__namespace.Mesh( new THREE__namespace.RingGeometry(0, 1, 16, 1, 0, Math.PI / 2), new THREE__namespace.MeshBasicMaterial({ color: this._thetaColor, transparent: true, opacity: 0.35, side: THREE__namespace.DoubleSide }) ); this.thetaWedge.material.depthTest = false; this.thetaWedge.renderOrder = 3; this.thetaWedge.rotation.set(Math.PI / 2, Math.PI / 2, 0); this.thetaLabel = new Label("0", "label angle-label"); this.thetaLabel.position.set(0, 1, 0); this.thetaLabelContainer = new THREE__namespace.Object3D(); this.thetaLabelContainer.add(this.thetaLabel); this.thetaWedge.add(this.thetaLabelContainer); this.phiWedge.add(this.thetaWedge); this.add(this.phiWedge, this.thetaWedge); this.scale.set(scale, scale, scale); this.phiColor = this._phiColor; this.thetaColor = this._thetaColor; this.labelRadius = 1.1; this.opacity = 0.2; } /** * Update the angle indicators for the given Bloch vector */ update(v) { const { phi, theta } = v; this.phiWedge.geometry.dispose(); this.thetaWedge.geometry.dispose(); this.phiWedge.geometry = new THREE__namespace.RingGeometry(0, 1, 16, 1, 0, phi); this.thetaWedge.geometry = new THREE__namespace.RingGeometry( 0, 1, 16, 1, Math.PI / 2, theta ); this.thetaWedge.rotation.set(Math.PI / 2, Math.PI / 2, 0); this.thetaWedge.rotateOnAxis(yAxis, Math.PI / 2 + phi); this.thetaLabelContainer.rotation.set(0, 0, Math.min(theta, 0.5)); this.phiLabelContainer.rotation.set(0, 0, phi / 2); if (isRadiansUnits(this.units)) { this.phiLabel.text = formatRadians(phi); this.thetaLabel.text = formatRadians(theta); } else { this.phiLabel.text = formatDegrees(phi); this.thetaLabel.text = formatDegrees(theta); } } get opacity() { return this.phiWedge.material.opacity; } set opacity(opacity) { this.phiWedge.material.opacity = opacity; this.thetaWedge.material.opacity = opacity; } /** * The distance of the labels from the center of the sphere */ get labelRadius() { return this.phiLabel.position.length(); } set labelRadius(radius) { this.phiLabel.position.set(radius, 0, 0); this.thetaLabel.position.set(0, radius, 0); } set color(color) { this.phiColor = color; this.thetaColor = color; } get phiColor() { return this._phiColor; } set phiColor(color) { this._phiColor.set(color); this.phiWedge.material.color = this._phiColor; this.phiLabel.color = this._phiColor; } get thetaColor() { return this._thetaColor; } set thetaColor(color) { this._thetaColor.set(color); this.thetaWedge.material.color = this._thetaColor; this.thetaLabel.color = this._thetaColor; } } const vertices = []; const radius = 1; const angle = Math.PI / 2; const segments = 32; for (let i = 0; i <= segments; i++) { const x = radius * Math.cos(i * angle / segments); const y = radius * Math.sin(i * angle / segments); vertices.push(x, y, 0); } vertices.push(0, 0, 0); vertices.push(radius, 0, 0); const geometry = new THREE__namespace.BufferGeometry(); geometry.setAttribute("position", new THREE__namespace.Float32BufferAttribute(vertices, 3)); geometry.dispose = () => { }; class Wedge extends BaseComponent { constructor() { super("wedge"); const material = new THREE__namespace.LineBasicMaterial({}); material.depthFunc = THREE__namespace.AlwaysDepth; const line = new THREE__namespace.Line(geometry, material); line.rotation.set(Math.PI / 2, 0, 0); this.add(line); } } class QubitProjWedge extends Wedge { constructor() { super(); } follow(v) { const { theta, phi } = v; if (theta > Math.PI / 2) { this.rotation.set(0, Math.PI, Math.PI - phi); } else { this.rotation.set(0, 0, phi); } } } class QubitDisplay extends BaseComponent { arrow; wedge; angleIndicators; state; _anim = null; constructor(q) { super("qubit-display"); this.arrow = new QubitArrow(); this.add(this.arrow); this.wedge = new QubitProjWedge(); this.angleIndicators = new AngleIndicators(); this.add(this.angleIndicators); this.state = BlochVector.zero(); if (q) { this.set(q); } } set color(color) { super.color = color; this.arrow.color = color; } /** * Set the bloch vector state of the display * * Can also be used to animate the state of the qubit. * * @param q - The new Bloch vector state to set. * @param duration - The duration of the animation (default is 0). * @param easing - The easing function to use for the animation (default is 'quadInOut'). */ set(q, duration = 0, easing = "quadInOut") { if (duration > 0) { const start = this.state.clone(); this._anim?.cancel(); this._anim = animate( (k) => { this.set(start.slerpTo(q, k)); }, duration, easing ); return this._anim; } else { this.state.copy(q); this.arrow.follow(q); this.wedge.follow(q); this.angleIndicators.update(q); } } } class BlochSpherePath extends THREE__namespace.Curve { from; to; constructor(from, to) { super(); this.from = from; this.to = to; } getPoint(t, optionalTarget = new THREE__namespace.Vector3()) { optionalTarget.copy(this.from.slerpTo(this.to, t)); return optionalTarget; } } function* pairs(arr) { for (let i = 0; i < arr.length - 1; i++) { yield [arr[i], arr[i + 1]]; } } function tubePath(vertices, material) { const curves = Array.from(pairs(vertices)).map(([v1, v2]) => new BlochSpherePath(v1, v2)).reduce((curvePath, curve) => { curvePath.add(curve); return curvePath; }, new THREE__namespace.CurvePath()); const tube = new THREE__namespace.TubeGeometry(curves, 256, 5e-3, 6, false); return new THREE__namespace.Mesh(tube, material); } class PathDisplay extends BaseComponent { constructor(path) { super("path-display"); if (path) { this.set(path); } } /// Set the path set(vertices) { this.clear(); const material = new THREE__namespace.MeshBasicMaterial({ color: defaultColors.path, side: THREE__namespace.DoubleSide, transparent: true, opacity: 0.8 }); material.depthTest = false; const mesh = tubePath(vertices, material); mesh.renderOrder = 10; this.add(mesh); } } const MAX_POINTS = 100; function sortRegionPoints(regionPoints) { let avgNormal = new THREE__namespace.Vector3(); regionPoints.forEach((p) => avgNormal.add(p)); avgNormal.normalize(); let ref = new THREE__namespace.Vector3(1, 0, 0); if (Math.abs(avgNormal.dot(ref)) > 0.9) ref.set(0, 1, 0); let tangentX = new THREE__namespace.Vector3().crossVectors(avgNormal, ref).normalize(); let tangentY = new THREE__namespace.Vector3().crossVectors(avgNormal, tangentX).normalize(); regionPoints.sort((a, b) => { let angleA = Math.atan2(a.dot(tangentY), a.dot(tangentX)); let angleB = Math.atan2(b.dot(tangentY), b.dot(tangentX)); return angleA - angleB; }); return regionPoints; } function getRegionPoints(region) { const points = []; for (const v of region) { points.push(v); } const sortedPoints = sortRegionPoints(points); for (let i = region.length; i < MAX_POINTS; i++) { sortedPoints.push(new THREE__namespace.Vector3(0, 0, 0)); } return sortedPoints; } class SphericalPolygonMaterial extends THREE__namespace.ShaderMaterial { region; constructor(region = []) { super({ uniforms: { regionPoints: { value: getRegionPoints(region) }, numPoints: { value: region.length }, highlightColor: { value: new THREE__namespace.Color(16711680).convertLinearToSRGB() } }, vertexShader: ` varying vec3 vPosition; void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vPosition = normalize(worldPosition.xyz); // Ensure it's on the sphere gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 regionPoints[${MAX_POINTS}]; uniform int numPoints; uniform vec3 highlightColor; varying vec3 vPosition; // Checks if point p is inside the spherical polygon defined by regionPoints bool insideRegion(vec3 p) { int winding = 0; vec3 averageNormal = vec3(0.0); for (int i = 0; i < numPoints; i++) { vec3 a = normalize(regionPoints[i]); vec3 b = normalize(regionPoints[(i + 1) % numPoints]); // Compute cross product and accumulate for average normal vec3 edgeNormal = cross(a, b); averageNormal += edgeNormal; if (dot(edgeNormal, p) > 0.0) { winding += 1; } else { winding -= 1; } } // Normalize to get the true average normal averageNormal = normalize(averageNormal); // Ensure point p is in the same hemisphere as averageNormal bool isCorrectHemisphere = dot(averageNormal, p) > 0.0; return abs(winding) == numPoints && isCorrectHemisphere; } void main() { if (insideRegion(vPosition)) { gl_FragColor = vec4(highlightColor, 1.0); } else { discard; } } `, transparent: true, side: THREE__namespace.DoubleSide }); this.region = region; } get highlightColor() { return this.uniforms.highlightColor.value; } set highlightColor(color) { this.uniforms.highlightColor.value = new THREE__namespace.Color( color ).convertLinearToSRGB(); } setRegion(region) { this.region = region; this.uniforms.regionPoints.value = getRegionPoints(region); this.uniforms.numPoints.value = region.length; } } class RegionDisplay extends BaseComponent { sphere; constructor(region) { super("region-display"); const material = new SphericalPolygonMaterial(); material.highlightColor = defaultColors.region; this.sphere = new THREE__namespace.Mesh( new THREE__namespace.SphereGeometry(0.985, 64, 64), material ); this.add(this.sphere); if (region) { this.setRegion(region); } } get color() { const material = this.sphere.material; return material.highlightColor; } set color(color) { const material = this.sphere.material; material.highlightColor = color; } /** * Set the region of the display * * @param points - The bloch vectors that define the region. */ setRegion(points) { const material = this.sphere.material; material.setRegion(points); } } class PointsDisplay extends BaseComponent { pointMaterial; constructor(points) { super("points-display"); this.pointMaterial = new THREE__namespace.ShaderMaterial({ vertexShader: ` uniform float pointSize; varying vec2 vUv; varying float distanceToCamera; void main() { vUv = uv; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); distanceToCamera = -mvPosition.z; gl_PointSize = pointSize; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 color; varying float distanceToCamera; void main() { vec2 coord = gl_PointCoord - vec2(0.5); // Center if (length(coord) > 0.5) discard; // Make it circular gl_FragColor = vec4(color, 1.0); } `, uniforms: { color: { value: new THREE__namespace.Color(1, 1, 1) }, pointSize: { value: 20 } } }); if (points) { this.set(points); } } /** * Set the size of the points */ get pointSize() { return this.pointMaterial.uniforms.pointSize.value; } set pointSize(size) { this.pointMaterial.uniforms.pointSize.value = size; } /** * Set the color of the points */ set color(color) { const colorValue = new THREE__namespace.Color(color); this.pointMaterial.uniforms.color.value = colorValue.convertLinearToSRGB(); } /** * Set the points to display */ set(points) { this.clear(); const positions = new Float32Array(points.length * 3); for (const [i, point] of points.entries()) { const pos = point; positions[i * 3] = pos.x; positions[i * 3 + 1] = pos.y; positions[i * 3 + 2] = pos.z; } const geometry = new THREE__namespace.BufferGeometry(); geometry.setAttribute("position", new THREE__namespace.BufferAttribute(positions, 3)); const pointsMesh = new THREE__namespace.Points(geometry, this.pointMaterial); this.add(pointsMesh); } } class Operator { elements; static identity() { return new Operator([ [Complex.ONE, Complex.ZERO], [Complex.ZERO, Complex.ONE] ]); } constructor(elements) { this.elements = elements; } /** * The first row, first column element of the operator */ get a() { return this.elements[0][0]; } /** * The first row, second column element of the operator */ get b() { return this.elements[0][1]; } /** * The second row, first column element of the operator */ get c() { return this.elements[1][0]; } /** * The second row, second column element of the operator */ get d() { return this.elements[1][1]; } copy(other) { this.elements = other.elements.map((row) => row.map((e) => e.clone())); return this; } clone() { return new Operator( this.elements.map((row) => row.map((e) => e.clone())) ); } /** * Multiply the operator by a scalar */ scale(scalar) { this.elements = this.elements.map((row) => row.map((e) => e.times(scalar))); return this; } /** * Multiply the operator by another operator */ times(other) { const a = this.a.times(other.a).plus(this.b.times(other.c)); const b = this.a.times(other.b).plus(this.b.times(other.d)); const c = this.c.times(other.a).plus(this.d.times(other.c)); const d = this.c.times(other.b).plus(this.d.times(other.d)); return new Operator([ [a, b], [c, d] ]); } /** * Get the conjugate transpose of the operator as a new operator */ conjugateTranspose() { return new Operator([ [this.a.conjugate(), this.c.conjugate()], [this.b.conjugate(), this.d.conjugate()] ]); } /** * Apply this operator to a density matrix */ applyTo(rho) { return this.times(new Operator(rho)).times(this.conjugateTranspose()).elements; } /** * Add another operator to this operator */ plus(other) { const a = this.a.plus(other.a); const b = this.b.plus(other.b); const c = this.c.plus(other.c); const d = this.d.plus(other.d); return new Operator([ [a, b], [c, d] ]); } /** * Get the determinant of the operator */ determinant() { return this.a.times(this.d).minus(this.b.times(this.c)); } /** * Get this operator as a THREE.Quaternion */ quaternion() { const halfI = Complex.I.times(0.5); const phase = this.determinant().pow(-0.5); const q0 = this.a.plus(this.d).times(phase.times(0.5)).real; const q1 = this.b.plus(this.c).times(phase.times(halfI)).real; const q2 = this.c.minus(this.b).times(phase.times(0.5)).real; const q3 = this.a.minus(this.d).times(phase.times(halfI)).real; const q = new THREE.Quaternion(q1, q2, q3, q0); return q.normalize(); } } class OperatorDisplay extends BaseComponent { operator; innerGroup; label; anim; constructor(op) { super("operator-display"); const innerGroup = new THREE__namespace.Group(); this.innerGroup = innerGroup; const cyl = new THREE__namespace.Mesh( new THREE__namespace.CylinderGeometry(5e-3, 5e-3, 1.05, 32), new THREE__namespace.MeshBasicMaterial({ color: defaultColors.operator, transparent: true, opacity: 0.5 }) ); cyl.rotation.x = Math.PI / 2; cyl.position.z = 0.525; innerGroup.add(cyl); const ringRadius = 0.7; const rings = new THREE__namespace.Group(); const ring = new THREE__namespace.Mesh( new THREE__namespace.RingGeometry( ringRadius - 0.01, ringRadius, 64, 1, 0, Math.PI / 2 ), new THREE__namespace.MeshBasicMaterial({ color: defaultColors.operator, side: THREE__namespace.DoubleSide, transparent: true, opacity: 0.5 }) ); ring.material.depthTest = false; ring.renderOrder = 5; ring.position.z = 0; const ring2 = ring.clone(); ring2.rotation.z = Math.PI; rings.add(ring); rings.add(ring2); const disc = new THREE__namespace.Mesh( new THREE__namespace.CircleGeometry(ringRadius - 0.01, 64), new THREE__namespace.MeshBasicMaterial({ color: defaultColors.operator, side: THREE__namespace.DoubleSide, transparent: true, opacity: 0.1 }) ); disc.material.depthTest = false; innerGroup.add(disc); this.anim = animate( (k) => { rings.rotation.z = Math.PI * 2 * k; }, 3e3, "linear", true ); innerGroup.add(rings); this.label = new Label(""); this.label.position.z = 1.1; innerGroup.add(this.label); this.add(innerGroup); this.renderOrder = 4; this.operator = Operator.identity(); if (op) { this.set(op); } } /** * Set the operator to display * @param op The operator to display */ set(op) { this.operator.copy(op); const q = this.operator.quaternion(); const info = axisFromQuaternion(q); if (info.angle == 0) { this.label.text = "Identity"; return; } this.quaternion.setFromUnitVectors(new THREE__namespace.Vector3(0, 0, 1), info.axis); this.label.text = `\u03B1 = ${formatDegrees(info.angle)}`; } /** * Perform cleanup tasks */ dispose() { this.anim.cancel(); } } class OperatorPathDisplay extends BaseComponent { operator; vector; innerGroup; path; disc; constructor(op, v) { super("operator-path-display"); const innerGroup = new THREE__namespace.Group(); this.innerGroup = innerGroup; this.path = new THREE__namespace.Mesh( new THREE__namespace.RingGeometry(0, 0.01, 64), new THREE__namespace.MeshBasicMaterial({ color: defaultColors.operatorPath, side: THREE__namespace.DoubleSide, transparent: true, opacity: 0.8 }) ); this.path.material.depthTest = false; innerGroup.add(this.path); this.disc = new THREE__namespace.Mesh( new THREE__namespace.CircleGeometry(1,