@qbead/bloch-sphere
Version:
A 3D Bloch Sphere visualisation built with Three.js and TypeScript.
1,802 lines (1,765 loc) • 53.9 kB
JavaScript
'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,