Animated visualization for 1-qubit and 2-qubit controls
Table of Contents
Overview
This is the base library/module of the Q-CTRL Visualizer. It is framework/library agnostic and can be implemented in a vanilla JavaScript app/website
Installation
Via NPM and ES6 import
- Login to npm from the command line:
npm login
your npm account must be linked and have access to the qtrl org (This will be unnecessary if the package becomes publicly available) - Install the Visualizer module into your project with
yarn add @qctrl/visualizer
or npm i @qctrl/visualizer
- Since the Visualizer is an npm module you will need to use the ES6 modules
import
feature and will also need a transpiler such as Babel and/or a build pipeline such as Parcel (easy) or Webpack (Advanced) - Import the Visualizer component into your App like so:
import Visualizer from '@qctrl/visualizer'
Via direct download of pre-built bundle
-
Clone or download the repo and find the file Visualizer-Module/build/visualizer.js
(or download/copy the built code directly from here)
-
Copy Visualizer.js
to your project and include it in your main .html
file, above your main/index .js
file using a <script>
tag like so:
...
<script src="path/to/visualizer.js"></script>
<script src="path/to/index.js"></script>
</body>
Quick Start Guide
Vanilla JavaScript implementation
-
In your project's JavaScript, get a reference to a <div>
element/DOM-Node that will be used as a wrapper for the visualizer (or create a new <div>
element). The Element should have width
and height
CSS properties set as the visualizer will use the wrapping <div>
width and height parameters to set it's own size parameters internally. Note: The wrapper element will have its style property position
set to relative
by the visualizer
<body>
...
<div id="visualizer-wrapper" style="width: 500px; height: 500px;"></div>
...
</body>
const visualizerWrapper = document.getElementById("visualizer-wrapper");
-
Next create a new Visualizer instance, passing in the wrapper reference, then initialize it like so:
const myVisualizer = new Visualizer({ wrapper: visualizerWrapper }).init();
-
This will create and initialize a new Visualizer object and store a reference to it in the myVisualizer
variable. Also, a canvas/DOM element that the visualizer will be rendered to will be attached to the passed in wrapper element
-
If you now preview the HTML file you should be able to see a single Bloch sphere on the page
-
A Visualizer instance has an update method that can be used to pass in new "props" or settings, for example you can set whether the animation is playing or not with the isPlaying
prop. To try that now, below from where you have created your Visualizer instance, use this code snippet to have the visualizer start playing when the page loads:
myVisualizer.update({ isPlaying: true });
-
Now when your HTML preview loads the Visualizer will start playing immediately. Since no data was provided to the Visualizer instance when you created it, it will have generated some default data: A single 180 degree rotation around the x axis. (If you have predefined data you can pass it in to the constructor when creating a new Visualizer instance and this will be used for the visualization. See the section on Data) for more info
-
If you do not have access to data you can instead pass in an array or a comma separated string of basic gate types. To try this, where you have initialized the visualizer instance update the code like so:
const myVisualizer = new Visualizer({
wrapper: visualizerWrapper,
gates: ["X", "H", "Z", "T"]
})
.init();
The Visualizer will generate the correct data to animate from the gate sequence you passed in. Now when you preview your HTML you will see an animated visualization of the above gate sequence. See Gates for more info and available gate types
Creating some basic playback controls
You can use a <button>
element and a range type <input>
element to create basic animation playback controls
Creating a Play/Pause button
-
First, get a reference to a button element and then add an onClick
event listener/callback to it that will update the visualizer's isPlaying
prop like so:
<body>
...
<button id="play-button">Play/Pause</button>
...
</body>
At the top of your javascript, below where you have created a reference to your wrapper element get the reference to the button element and create a variable isPlaying
that will be the reference for the isPlaying state in your app:
const playButton = document.getElementById("play-button");
let isPlaying = false;
Then, below where you have initialized your visualizer, remove the earlier call to myVisualizer.update()
and replace with this code snippet:
playButton.addEventListener("click", () => {
isPlaying = !isPlaying;
myVisualizer.update({ isPlaying });
});
Now when you load your HTML preview there will be a clickable button to start and stop the visualization animation
Creating a playback indicator (play bar)
The play bar will have the ability to scrub or jump to a particular place in the animation as well as visually indicating the position/progress of the animation
-
For this you will need to create a callback function and pass it into the visualizer constructor using the onUpdate
prop. The callback you pass in will be called every browser animation frame and will be called with an event argument containing a reference to the visualizer instance itself (for convenient access to the update method) and a data object containing some specific and useful information such as the progress (calculated from the visualizer's current internal animation frame). See onUpdate for more info
-
Create and pass in the onUpdate
callback like so Note: Replace the previous code where you created and initialized a new Visualizer instance with the following:
const visualizerOnUpdate = event => {
};
const myVisualizer = new Visualizer({
wrapper: visualizerWrapper,
gates: ["X", "H", "Z", "T"],
onUpdate: visualizerOnUpdate
}).init();
-
Now, get a reference to a range type input element (slider). Since the progress is represented as a percentage in decimal format, the <input>
element will need a min
of 0 and max
of 1 along with an appropriate step
value for it to function properly (you could also use 0 to 100 and then convert the value back to a decimal percentage before passing it as progress
into the visualizer update method):
<body>
...
<input id="play-bar" type="range" min="0" max="1" step="0.01" />
...
</body>
At the top of your javascript, below where you have created a reference to your button element, get a reference to your input element and create a variable progress
that will be the reference for the progress state in your app:
const playBar = document.getElementById("play-bar");
let progress = 0;
-
Back where you wrote the onUpdate
callback function, fill the body of the function in like so:
const visualizerOnUpdate = event => {
progress = event.data.progress;
playBar.value = progress;
};
-
Next, to set up the ability to jump or 'scrub' through the animation, below where you added the click event listener to the button, add an "on input" event listener to the playBar
element like so:
playBar.addEventListener("input", e => {
progress = +e.target.value;
myVisualizer.update({ progress });
});
-
Finally, to have the play bar reset to the start when the animation has completed, update the event listener for the play/pause button like so:
playButton.addEventListener("click", () => {
isPlaying = !isPlaying;
if (progress >= 1) {
isPlaying = true;
progress = 0;
}
myVisualizer.update({ isPlaying, progress });
});
Now when you load your HTML preview, you will see a slider, it's position/value will update based on the progress of the animation after clicking the play/pause button. You can also click on the slider or drag the handle to move or "scrub" through the animation
Final javascript for reference:
const visualizerWrapper = document.getElementById("visualizer-wrapper");
const playButton = document.getElementById("play-button");
const playBar = document.getElementById("play-bar");
let isPlaying = false;
let progress = 0;
const visualizerOnUpdate = event => {
progress = event.data.progress;
playBar.value = progress;
};
const myVisualizer = new Visualizer({
wrapper: visualizerWrapper,
gates: ["X", "H", "Z", "T"],
onUpdate: visualizerOnUpdate
}).init();
playButton.addEventListener("click", () => {
isPlaying = !isPlaying;
if (progress >= 1) {
isPlaying = true;
progress = 0;
}
myVisualizer.update({ isPlaying, progress });
});
playBar.addEventListener("input", e => {
progress = +e.target.value;
myVisualizer.update({ progress });
});
API
Exports (NPM package)
AVAILABLE_CONFIGURATIONS
If using the build file this will be available as .AVAILABLE_CONFIGURATIONS
on the Visualizer instance
An object whose values are the names of the currently available layout configurations. These configurations are used internally based on the data passed in however you can explicitly set the layout with the configurationName
prop
{
"ONE_QUBIT": "oneQubit",
"TWO_QUBIT_ENTANGLEMENT": "twoQubitEntanglement"
}
AVAILABLE_CAMERA_PRESETS
If using the build file this will be available as .AVAILABLE_CAMERA_PRESETS
on the Visualizer instance
An array of names of available camera preset positions that can be used with the cameraPreset
prop
["XY", "YZ", "XZ", "DEFAULT"];
AVAILABLE_THEME_CONTROLS
If using the build file this will be available as .AVAILABLE_THEME_CONTROLS
on the Visualizer instance
An array of objects (collection) of all available theming settings. This can be used to build a UI for controlling the theme settings that get passed in on the themeSettings
prop. Each object will have at least a name
and a type
key:value. The name
value corresponds to and matches with the name that should be passed in on the themeSettings
prop to control/change that setting. The type
is the type of suggested <input>
and expected value for the themeSettings
prop. See Styling and Theming for more info.
Depending on the type there may be more values in the "available control" object, see below.
Theme Control Types
color
- Recommended to be used with a color input/color picker that returns CSS color string values
rangeValue
- Recommended to be used with a range (slider) input, includes
min
max
and step
values to use for the range input
numberValue
- Recommended to be used with a number input, includes
min
max
and step
values to use for the number input
boolean
- Recommended to be used with a toggle like control such as a checkbox input
Below is a subset of the currently available theme controls as an example of what they will look like:
[
{
name: "elementMeshColor",
type: "color"
},
{
name: "outerLightIntensity",
type: "rangeValue",
min: 0,
max: 15,
step: 0.01
},
{
name: "pathWeight",
type: "numberValue",
min: 1,
max: 10,
step: 1
},
{
name: "showOutlines",
type: "boolean"
}
];
default
: Visualizer class / constructor
If using the build file this will be available as a global var (attached to the window object as a property) as Visualizer
This is the default export from the library and is used with the new
keyword to create a new visualizer instance
Props (constructor
/ .init()
/ .update()
)
Visualizer Control Props
These props can be used to control the playback, camera and label display of the visualization
cameraPreset
: <string>
Can be passed to: constructor
, .init()
, .update()
Sets the camera/3D view to a preset axial position
"XY"
"YZ"
"XZ"
"XYZ"
"DEFAULT"
isPlaying
: <boolean>
default
: false
Can be passed to: constructor
, .init()
, .update()
Tells the Bloch sphere component to start or stop the animation
Note:*isPlaying
will be set to false internally when the visualization animation reaches the last frame*
labels
: <object<JSON>>
Can be passed to: constructor
, .init()
, .update()
Set the visibility of individual labels
JSON structure:
{
"xAxis": <boolean>,
"yAxis": <boolean>,
"zAxis": <boolean>,
"theta": <boolean>,
"phi": <boolean>,
"northPole": <boolean>,
"southPole": <boolean>,
"nonErrorState": <boolean>
}
progress
: <float ( >= 0 <= 1 )>
default
: 0
Can be passed to: constructor
, .init()
, .update()
Sets the progress of the animation as a percent in decimal format (0 - 1). The animation frame to display will be updated accordingly. The actually frame index will be calculated internally based off the progress
Callbacks
getInitialThemeSettings
: <function>
Can be passed to: constructor
This callback is handy if you are trying to sync UI elements in your app to match the initial values for the theme settings on page/component load.
This callback will get all the initial settings for any available theme controls. If passed in to the Visualizer constructor when creating a new instance, the callback will be fired once during the calling of the constructor. It will be called with an object as the only argument and the object will contain key:values of the initial themeable settings.
onUpdate
: <function>
Can be passed to: constructor
, .init()
This callback, if passed in will be fired each browser animation cycle and is called with an event object:
-
event.target
is a handy reference to the visualizer instance
-
event.data
contains immediately accessible info/data about the state of the visualizer instance:
{
currentSegmentNumber,
frameIndex,
progress,
isInteracting,
isHovering;
}
visualizationData
: <object<JSON>>
Can be passed to: constructor
, .update()
There can be two possible keys of the visualizationData
object representing sub objects containing data sets:
data
which will be the standard data set or the error state data set if you pass in a nonErrorStateData
set. This key is requirednonErrorStateData
Is an optional data set that if provided, will be visualized as a secondary indicator with a path between it and the main indicator to show decoherence. Same structure as the main data object. Note that the names of the qubits and vectors in nonErrorStateData
must match with those on the data
key
Visualization data passed in on either the data
key or the nonErrorStateData
key must be of a particular format which will be detailed below.
NOTE: The layout configuration for the visualization is determined automatically based on the number of qubits under the qubits
key, unless a custom configuration is being passed in
NOTE: If updating data in the .update()
method, the names of the qubits and vectors must match with those in the first data set passed in to the constructor.
Data Keys
qubits
required: <array<object>>
-
qubits
is the only required key
-
qubits
data will be visualized using bloch spheres
-
qubits
must be an array of objects (a collection), each object must have the following format:
{ "name": "<qubitName>", "x": [], "y": [], "z": [] }
- Where the
x
y
and z
key values are arrays, that must be of equal length, of the plot points to be visualized, such that a complete plot point vector will be for example: (x[0], y[0], z[0])
- The
name
key value can be any camel cased name - Note: The order of multiple qubits in the array will be the order in which they are rendered in the Visualization/to the screen, from left to right
vectors
: <array<object>>
- Vectors represent entangled states between two qubits and will be visualized on tetrahedra
vectors
must follow the same format as qubits
(see above)- Data for the first vector should represent x: XY y: YZ z: ZX
- Data for the second vector should represent x: XZ y: YX z: ZY
- Data for the third vector should represent x: XX y: YY z: ZZ
- Multiple vectors will be rendered from left to right, top to bottom in the order that they are in in the
vectors
array
segmentIndexes
: <array<integer>>
- This must be an array of integers starting at
1
(0
will not be highlighted as a segment) - The array length must be equal to the array lengths of the plot points arrays for
qubits
and vectors
- Segments will be highlighted as they are being animated
entanglementBooleans
: <array<boolean>>
- This must be an array of booleans and will be visualized by an entanglement link icon, showing whether there is an entangled state between two qubits
- The array length must be equal to the array lengths of the plot points arrays for
qubits
and vectors
entanglementMeasures
: <array<float ( >= 0 <= 1 )>>
- This must be an array of floats between 0 and 1, representing the percentage of entanglement, visualized by an animated and gradated bar
- The array length must be equal to the array lengths of the plot points arrays for
qubits
and vectors
Below is an example (excluding the actual data points) of the full data object structure for a two qubit visualization with entanglement:
{
"qubits": [
{
"name": "<qubit_1_name>",
"x": [],
"y": [],
"z": []
},
{
"name": "<qubit_2_name>",
"x": [],
"y": [],
"z": []
}
],
"vectors": [
{
"name": "<vector_a_name>",
"x": [],
"y": [],
"z": []
},
{
"name": "<vector_b_name>",
"x": [],
"y": [],
"z": []
},
{
"name": "<vector_c_name>",
"x": [],
"y": [],
"z": []
}
],
"segmentIndexes": [],
"entanglementBooleans": [],
"entanglementMeasures": []
}
Other Props
configurationName
: <string>
Can be passed to: constructor
You can pass in one of the available configurations names as a string to explicitly set the desired layout configuration. This will override the configuration that is automatically chose n based on the data
continuousSegments
: <boolean>
default
: true
Can be passed to: constructor
, .init()
, .update()
If set to false, a path/line piece will not be created/drawn between each segment (useful if there are non-contiguous segment indexes in a data set. Note that if passing this prop into .update()
, it will only have an effect if updating the data on the same call to .update()
drawArcs
: <boolean>
default
: false
Can be passed to: constructor
, .init()
, .update()
If set to true, when creating the pulse path, an arc of extra points/vectors will be generated and drawn between each data point instead of directly connecting each point with a straight line. Useful if using a sparse set of data, where the distance between each data point is large (for example with certain noise or slepian visualizations). Note that if passing this prop into .update()
, it will only have an effect if updating the data on the same call to .update()
gates
: <array<string>> | <string>
Can be passed to: constructor
, .init()
Instead of a predefined data object you can pass in an array or a comma separated string of basic gate types and the Visualizer will generate the correct data to animate from the gate sequence. Note that if you pass in both data
and gates
as props the data
will be used and the gates ignored.
NOTE: Currently only works for a one qubit visualization
Possible gate operations are (case insensitive):
X
Y
Z
H
S
S_DAG
(S†, The conjugate transpose of the S gate)T
T_DAG
(T†, the conjugate transpose of the T gate)
Example gates
array:
["X", "H", "Z", "T", "S_DAG"];
Example gates
string:
"X,H,Z,T,S_DAG";
inputState
: <object<JSON>>
Can be passed to: constructor
, .init()
, .update()
This will set an arbitrary starting / position for the pulse indicators
JSON structure:
{
"qubits": [
{ "name": "qubit1", "x": 0, "y": 1, "z": 0 },
{ "name": "qubit2", "x": 0, "y": 0, "z": -1 }
],
"vectors": [{ "name": "a", "x": 0, "y": 0, "z": -1 }]
}
layoutConfiguration
: <object<HTMLElement | DOMNode>>
Can be passed to: constructor
, .init()
Provide a custom layout to override the internally generated one. Each key should be a string matching a "qubit" or "vector" name in the data provided, the values should be div elements with both their CSS properties "grid-area" and their id attributes set to the same name as the key
style
: <object<JSON>>
Can be passed to: constructor
- See the file
./src/configuration/styleDefaults.js
to see what structure layout settings need to be in - If no
style
prop is passed, all default settings will be used from styleDefaults.js
- You are able to pass a subset of layout settings and these will be merged with the rest of the default settings from the settings file, no need to pass all the settings if you only need to customize a few things
See Styling and Theming for more info
themeSettings
: <object>
Can be passed to: .update()
The themeSettings
prop is used to dynamically change certain visual styles of the visualizer instance. In effect you can create your own color and visual theme for your visualization by adjusting these settings. pass in one or more key:value pairs where the key is the name of an available theme setting and the value is the new value you would like to set for that setting
Possible theme settings with types:
{
"outerLightIntensity": <float>,
"innerLightIntensity": <float>,
"backgroundColor": <string:cssColor>,
"indicatorColor": <string:cssColor>,
"pathColor": <string:cssColor>,
"highlightColor": <string:cssColor>,
"nonErrorStateDotColor": <string:cssColor>,
"metalness": <float>,
"roughness": <float>,
"opacity": <float>,
"elementMeshColor": <string:cssColor>,
"pathWeight": <integer>,
"showOutlines": <boolean>
}
See available theme controls for information on how to get references to theme control types and accepted ranges for control values
wrapper
required: <HTMLElement | DOMNode>
Must be passed to either: constructor
or .init()
This is the only required prop for a new visualizer instance. It must be a valid HTML element/DOM node. The width
and height
style/CSS properties must be set as the visualizer will use the wrapper width and height parameters to set it's own size parameters internally. It will then attach the canvas/rendering context to it. Note: The wrapper element will have its style property position
set to relative
by the visualizer
Available Visualizer instance Methods
.init({ ...props })
Once you have created a new Visualizer instance, you must call the .init()
method to start the internal renderer and of the visualizer
Note: This method will only ever fire once regardless of how many times it is called. This is handy if you are using or developing a wrapping component using a particular front-end framework or library that uses life cycle methods and re-renders and you only have access to a valid DOM node/ref for the wrapper
on the first component update (after any mounting cycles). You can call .init()
in the update cycle with the valid ref and not need to worry about creating a conditional to only call it once.
Valid props:
isPlaying
labels
progress
onUpdate
gates
inputState
layoutConfiguration
wrapper
Note: Props that are valid for both the constructor
and .init()
should only be passed into one or the other
.update({ ...props })
This is the method used to control the the play back state including progress, labels state, camera preset and visual theme settings
Valid props:
isPlaying
labels
progress
inputState
cameraPreset
themeSettings
.getProgressFromFrameIndex()
You can use this method to get the current progress of the visualizer animation as a decimal percent without relying on the onUpdate()
callback. This Method takes no arguments.
.cleanup()
This method should be called if you are replacing a visualizer instance with a new one (like unmounting and re-mounting), such as you would with a single page application. By calling this method before loosing the reference to the visualizer instance, a cleanup routine, disposing of expensive 3D geometry in the 3d context, will be run. This can help with memory and performance issues. This Method takes no arguments.
Styling and Theming
Development
JSDocs
Dev/Build/Publish Workflow