diff --git a/.prettierignore b/.prettierignore index 8cc94b7af4..d62a7667df 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,6 +9,8 @@ __pycache__/ .vscode-test-web/ .venv/ /allocator/ +/circuit_vis/lib/ +/circuit_vis/dist/ /compiler/ /jupyterlab/lib/ /jupyterlab/qsharp-jupyterlab/labextension/ diff --git a/build.py b/build.py index 5a748a10f4..aefa575b6b 100755 --- a/build.py +++ b/build.py @@ -69,10 +69,10 @@ ) parser.add_argument( - "--ci-bench", - action=argparse.BooleanOptionalAction, - default=False, - help="Run the benchmarking script that is run in CI (default is --no-ci-bench)", + "--ci-bench", + action=argparse.BooleanOptionalAction, + default=False, + help="Run the benchmarking script that is run in CI (default is --no-ci-bench)", ) args = parser.parse_args() @@ -117,6 +117,7 @@ wasm_bld = os.path.join(root_dir, "target", "wasm32", build_type) samples_src = os.path.join(root_dir, "samples") npm_src = os.path.join(root_dir, "npm", "qsharp") +circuit_vis_src = os.path.join(root_dir, "circuit_vis") play_src = os.path.join(root_dir, "playground") pip_src = os.path.join(root_dir, "pip") widgets_src = os.path.join(root_dir, "widgets") @@ -303,25 +304,46 @@ def run_python_integration_tests(cwd, interpreter): def run_ci_historic_benchmark(): branch = "main" output = subprocess.check_output( - ["git", "rev-list", "--since=1 week ago", "--pretty=format:%ad__%h", "--date=short", branch] + [ + "git", + "rev-list", + "--since=1 week ago", + "--pretty=format:%ad__%h", + "--date=short", + branch, + ] ).decode("utf-8") - print('\n'.join([line for i, line in enumerate(output.split('\n')) if i % 2 == 1])) + print("\n".join([line for i, line in enumerate(output.split("\n")) if i % 2 == 1])) output = subprocess.check_output( - ["git", "rev-list", "--since=1 week ago", "--pretty=format:%ad__%h", "--date=short", branch] + [ + "git", + "rev-list", + "--since=1 week ago", + "--pretty=format:%ad__%h", + "--date=short", + branch, + ] ).decode("utf-8") - date_and_commits = [line for i, line in enumerate(output.split('\n')) if i % 2 == 1] + date_and_commits = [line for i, line in enumerate(output.split("\n")) if i % 2 == 1] for date_and_commit in date_and_commits: print("benching commit", date_and_commit) result = subprocess.run( - ["cargo", "criterion", "--message-format=json", "--history-id", date_and_commit], + [ + "cargo", + "criterion", + "--message-format=json", + "--history-id", + date_and_commit, + ], capture_output=True, - text=True + text=True, ) with open(f"{date_and_commit}.json", "w") as f: f.write(result.stdout) + if build_pip: step_start("Building the pip package") @@ -357,23 +379,6 @@ def run_ci_historic_benchmark(): step_end() -if build_widgets: - step_start("Building the Python widgets") - - widgets_build_args = [ - sys.executable, - "-m", - "pip", - "wheel", - "--no-deps", - "--wheel-dir", - wheels_dir, - widgets_src, - ] - subprocess.run(widgets_build_args, check=True, text=True, cwd=widgets_src) - - step_end() - if build_wasm: step_start("Building the wasm crate") # wasm-pack can't build for web and node in the same build, so need to run twice. @@ -394,6 +399,16 @@ def run_ci_historic_benchmark(): ) step_end() +if build_npm: + step_start("Building the circuit_vis package") + + npm_args = [npm_cmd, "install"] + subprocess.run(npm_args, check=True, text=True, cwd=circuit_vis_src) + + npm_args = [npm_cmd, "run", "build:prod"] + subprocess.run(npm_args, check=True, text=True, cwd=circuit_vis_src) + step_end() + if build_npm: step_start("Building the npm package") # Copy the wasm build files over for web and node targets @@ -422,6 +437,23 @@ def run_ci_historic_benchmark(): subprocess.run(npm_test_args, check=True, text=True, cwd=npm_src) step_end() +if build_widgets: + step_start("Building the Python widgets") + + widgets_build_args = [ + sys.executable, + "-m", + "pip", + "wheel", + "--no-deps", + "--wheel-dir", + wheels_dir, + widgets_src, + ] + subprocess.run(widgets_build_args, check=True, text=True, cwd=widgets_src) + + step_end() + if build_play: step_start("Building the playground") play_args = [npm_cmd, "run", "build"] diff --git a/circuit_vis/.gitignore b/circuit_vis/.gitignore new file mode 100644 index 0000000000..34a3ea2900 --- /dev/null +++ b/circuit_vis/.gitignore @@ -0,0 +1,156 @@ +## Ignore Node.js temporary files, build results, and +## files generated by popular Node.js add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test +.*[v]env/ + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +## Project-specific files ## + +# NPM's package-lock.json +package-lock.json + +# VSCode configuration +.vscode + +# IntelliJ +.idea + +# macOS metadata folder +.DS_Store + +# Compiled files +lib +.tgz + +# Python +.mypy_cache/ +*/.coverage +*/.coverage.* +*/.nox/ +*/.python-version +*/.pytype/ +/dist/ +*/docs/_build/ +/src/*.egg-info/ +__pycache__/ +*/.cookiecutter.json +*-checkpoint.ipynb +*/*.xml +*.xml diff --git a/circuit_vis/.prettierrc.js b/circuit_vis/.prettierrc.js new file mode 100644 index 0000000000..7780e14a1e --- /dev/null +++ b/circuit_vis/.prettierrc.js @@ -0,0 +1,17 @@ +module.exports = { + semi: true, + trailingComma: "all", + singleQuote: true, + endOfLine: "lf", + printWidth: 120, + tabWidth: 4, + parser: "typescript", + overrides: [ + { + files: ["*.md", "*.json", "*.yml", "*.yaml"], + options: { + tabWidth: 2, + }, + }, + ], +}; diff --git a/circuit_vis/README.md b/circuit_vis/README.md new file mode 100644 index 0000000000..9a0d5dd57c --- /dev/null +++ b/circuit_vis/README.md @@ -0,0 +1,107 @@ +# quantum-viz.js + +[![Licensed under the MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt) +[![PR's Welcome](https://img.shields.io/badge/PRs%20-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Build Status](https://github.com/microsoft/quantum-viz.js/actions/workflows/main.yml/badge.svg)](https://github.com/microsoft/quantum-viz.js/actions) +[![npm version](https://badge.fury.io/js/%40microsoft%2Fquantum-viz.js.svg)](https://www.npmjs.com/package/@microsoft/quantum-viz.js) + +

+ screenshot of circuit generated by quantum-viz +

+ +**quantum-viz.js** (or **qviz**) is a configurable tool for rendering quantum circuits. With the increasing demand for quantum libraries and educational tools, quantum circuits provide an intuitive way to visualize and understand quantum algorithms. quantum-viz.js is a lightweight library that can be easily integrated into any project. It aims to be easily configurable while allowing complex user interactions, such as toggling between different measurement outcomes. + +## Getting Started + +### Installation + +Include `quantum-viz.js` in your HTML page by using it directly from CDN: + +```html + +``` + +or import it in into your TypeScript package: + +```bash +npm i @microsoft/quantum-viz.js +``` + +### Usage + +1. Create a `Circuit` JavaScript object (the `Circuit` schema is documented [here](https://github.com/microsoft/quantum-viz.js/wiki/API-schema-reference)): + +```js +const sampleCircuit = { + qubits: [ + // ... + ], + operations: [ + // ... + ], +}; +``` + +2. Draw it in a `div`: + +```js +const sampleDiv = document.getElementById("sample"); +qviz.draw(sampleCircuit, sampleDiv, qviz.STYLES["Default"]); +``` + +Refer to the [`example`](./example) folder for an example on how to use quantum-viz.js. Notice that in order to open the contents of this folder in a browser you will need first to install from source (see [below](#running-from-source)). + +## Python usage + +To use this package with Python, use [quantum-viz](/quantum-viz). + +## Running from source + +### Installing + +To build and install this project from source, run the following commands from the root folder of this repository: + +```bash +# Install dependencies +> npm install +# Build +> npm run build:prod +``` + +### Running tests + +To run tests for this project, run the following commands: + +```bash +# Installs dependencies (run this step if you haven't) +> npm install +# Starts Jest tests +> npm run test +``` + +## Contributing + +Check out our [contributing guidelines](CONTRIBUTING.md) to find out how you can contribute to quantum-viz.js! + +## Feedback + +If you have feedback about this library, please let us know by filing a [new issue](https://github.com/microsoft/quantum-viz.js/issues/new/choose)! + +## Reporting Security Issues + +Security issues and bugs should be reported privately, via email, to the Microsoft Security +Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should +receive a response within 24 hours. If for some reason you do not, please follow up via +email to ensure we received your original message. Further information, including the +[MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in +the [Security TechCenter](https://technet.microsoft.com/en-us/security/default). + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/circuit_vis/SECURITY.md b/circuit_vis/SECURITY.md new file mode 100644 index 0000000000..1488eb5dc9 --- /dev/null +++ b/circuit_vis/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/circuit_vis/__tests__/__snapshots__/gateFormatter.test.ts.snap b/circuit_vis/__tests__/__snapshots__/gateFormatter.test.ts.snap new file mode 100644 index 0000000000..e75b6f54d1 --- /dev/null +++ b/circuit_vis/__tests__/__snapshots__/gateFormatter.test.ts.snap @@ -0,0 +1,2991 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing _classicalControlled No htmlClass 1`] = ` + + + + + + + + + X + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled change padding 1`] = ` + + + + + + + + + X + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled multiple 'zero'/'one' children 1`] = ` + + + + + + + + X + + + + + + + + + + Z + + + + + + + + + + + + H + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled nested children 1`] = ` + + + + + + + + + X + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled one 'one' child 1`] = ` + + + + + + + + + X + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled one 'zero' child 1`] = ` + + + + + + + + X + + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _classicalControlled one 'zero'/'one' child 1`] = ` + + + + + + + + X + + + + + + + + + + + + H + + + + + + + + + + + ? + + + + +`; + +exports[`Testing _controlledGate CNOT gate 1`] = ` + + + + + + + + + +`; + +exports[`Testing _controlledGate CNOT gate 2`] = ` + + + + + + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 1 control + 1 target 1`] = ` + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 1 control + 1 target 2`] = ` + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 1 control + 2 targets 1`] = ` + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 1 control + 2 targets 2`] = ` + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 1 control + 2 targets 3`] = ` + + + + + + + + Foo + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 2 controls + 2 targets 1`] = ` + + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 2 controls + 2 targets 2`] = ` + + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 2 controls + 2 targets 3`] = ` + + + + + + + + + Foo + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with 2 controls + 2 targets 4`] = ` + + + + + + + + + Foo + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with multiple controls + 1 target 1`] = ` + + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with multiple controls + 1 target 2`] = ` + + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate Controlled U gate with multiple controls + 1 target 3`] = ` + + + + + + + + + Foo + + + + +`; + +exports[`Testing _controlledGate SWAP gate 1`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _controlledGate SWAP gate 2`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _controlledGate SWAP gate 3`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _createGate Expanded gate 1`] = ` + + + + + + + +`; + +exports[`Testing _formatGate CNOT gate 1`] = ` + + + + + + + + + +`; + +exports[`Testing _formatGate classically controlled gate 1`] = ` + + + + + + + ? + + + + +`; + +exports[`Testing _formatGate controlled swap gate 1`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _formatGate controlled unitary gate 1`] = ` + + + + + + + + U + + + + +`; + +exports[`Testing _formatGate controlled unitary gate with arguments 1`] = ` + + + + + + + + U + + + ('foo', 'bar') + + + + +`; + +exports[`Testing _formatGate gate with metadata 1`] = ` + + + + + + H + + + + +`; + +exports[`Testing _formatGate measure gate 1`] = ` + + + + + + + +`; + +exports[`Testing _formatGate multi-qubit unitary gate 1`] = ` + + + + + + U + + + + +`; + +exports[`Testing _formatGate multi-qubit unitary gate with arguments 1`] = ` + + + + + + + U + + + ('foo', 'bar') + + + + +`; + +exports[`Testing _formatGate single-qubit unitary gate 1`] = ` + + + + + + H + + + + +`; + +exports[`Testing _formatGate single-qubit unitary gate with arguments 1`] = ` + + + + + + Ry + + + (0.25) + + + + +`; + +exports[`Testing _formatGate swap gate 1`] = ` + + + + + + + + + + + + + + +`; + +exports[`Testing _groupedOperations children on consecutive registers 1`] = ` + + + + + + + + + X + + + + + + + + + + H + + + + + + +`; + +exports[`Testing _groupedOperations children on non-consecutive registers 1`] = ` + + + + + + + + + X + + + + + + + + + + H + + + + + + +`; + +exports[`Testing _groupedOperations children on same register 1`] = ` + + + + + + + + + X + + + + + + + + + + Z + + + + + + +`; + +exports[`Testing _groupedOperations multiple children 1`] = ` + + + + + + + + + X + + + + + + + + + + Z + + + + + + + + + + H + + + + + + +`; + +exports[`Testing _groupedOperations nested children 1`] = ` + + + + + + + + + + + + X + + + + + + + + + + Z + + + + + + + + + + + + H + + + + + + +`; + +exports[`Testing _groupedOperations one child 1`] = ` + + + + + + + + + X + + + + + + +`; + +exports[`Testing _measure 1 qubit + 1 classical registers 1`] = ` + + + + + +`; + +exports[`Testing _measure 2 qubit + 1 classical registers 1`] = ` + + + + + +`; + +exports[`Testing _measure 2 qubit + 2 classical registers 1`] = ` + + + + + +`; + +exports[`Testing _measure 2 qubit + 2 classical registers 2`] = ` + + + + + +`; + +exports[`Testing _swap Adjacent swap 1`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _swap Adjacent swap 2`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _swap Non-adjacent swap 1`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _swap Non-adjacent swap 2`] = ` + + + + + + + + + + + + +`; + +exports[`Testing _unitary Multiqubit unitary on consecutive registers 1`] = ` + + + + + ZZ + + + +`; + +exports[`Testing _unitary Multiqubit unitary on consecutive registers 2`] = ` + + + + + ZZZ + + + +`; + +exports[`Testing _unitary Multiqubit unitary on non-consecutive registers 1`] = ` + + + + + + ZZ + + + + + + ZZ + + + +`; + +exports[`Testing _unitary Multiqubit unitary on non-consecutive registers 2`] = ` + + + + + + ZZZ + + + + + + ZZZ + + + +`; + +exports[`Testing _unitary Multiqubit unitary on non-consecutive registers 3`] = ` + + + + + ZZ + + + + + + ZZ + + + +`; + +exports[`Testing _unitary Multiqubit unitary on non-consecutive registers 4`] = ` + + + + + ZZZ + + + + + + ZZZ + + + +`; + +exports[`Testing _unitary Single qubit unitary 1`] = ` + + + + + H + + + +`; + +exports[`Testing _zoomButton Expanded gate 1`] = `null`; + +exports[`Testing _zoomButton Expanded with children gate 1`] = `null`; + +exports[`Testing _zoomButton Non-expanded with children gate 1`] = `null`; + +exports[`Testing _zoomButton Non-expanded with no children gate 1`] = `null`; + +exports[`Testing formatGates Multiple gates 1`] = ` + + + + + + + + + + + + + + + + + + X + + + + + + + + + + X + + + + + + + + + + + + +`; + +exports[`Testing formatGates Single gate 1`] = ` + + + + + + + + + + + +`; diff --git a/circuit_vis/__tests__/__snapshots__/inputFormatter.test.ts.snap b/circuit_vis/__tests__/__snapshots__/inputFormatter.test.ts.snap new file mode 100644 index 0000000000..fc1914accb --- /dev/null +++ b/circuit_vis/__tests__/__snapshots__/inputFormatter.test.ts.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing _qubitInput classical register 1`] = ` + + |0⟩ + +`; + +exports[`Testing _qubitInput classical register 2`] = ` + + |0⟩ + +`; + +exports[`Testing _qubitInput classical register 3`] = ` + + |0⟩ + +`; + +exports[`Testing formatInputs 1 quantum register 1`] = ` + + + |0⟩ + + +`; + +exports[`Testing formatInputs Multiple quantum registers 1`] = ` + + + |0⟩ + + + |0⟩ + + + |0⟩ + + +`; + +exports[`Testing formatInputs Quantum and classical registers 1`] = ` + + + |0⟩ + + + |0⟩ + + + |0⟩ + + +`; + +exports[`Testing formatInputs Quantum and classical registers 2`] = ` + + + |0⟩ + + + |0⟩ + + + |0⟩ + + +`; + +exports[`Testing formatInputs Skip quantum registers 1`] = ` + + + |0⟩ + + + |0⟩ + + +`; diff --git a/circuit_vis/__tests__/__snapshots__/registerFormatter.test.ts.snap b/circuit_vis/__tests__/__snapshots__/registerFormatter.test.ts.snap new file mode 100644 index 0000000000..3487917ad4 --- /dev/null +++ b/circuit_vis/__tests__/__snapshots__/registerFormatter.test.ts.snap @@ -0,0 +1,1786 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing _classicalRegister register with label offset 1`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with label offset 2`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with label offset 3`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with large width 1`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with large width 2`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with large width 3`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with normal width 1`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with normal width 2`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with normal width 3`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with small width 1`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with small width 2`] = ` + + + + + + +`; + +exports[`Testing _classicalRegister register with small width 3`] = ` + + + + + + +`; + +exports[`Testing _qubitRegister register with label offset 1`] = ` + + + + q0 + + +`; + +exports[`Testing _qubitRegister register with label offset 2`] = ` + + + + q1 + + +`; + +exports[`Testing _qubitRegister register with label offset 3`] = ` + + + + q2 + + +`; + +exports[`Testing _qubitRegister register with large width 1`] = ` + + + + q0 + + +`; + +exports[`Testing _qubitRegister register with large width 2`] = ` + + + + q1 + + +`; + +exports[`Testing _qubitRegister register with large width 3`] = ` + + + + q2 + + +`; + +exports[`Testing _qubitRegister register with normal width 1`] = ` + + + + q0 + + +`; + +exports[`Testing _qubitRegister register with normal width 2`] = ` + + + + q1 + + +`; + +exports[`Testing _qubitRegister register with normal width 3`] = ` + + + + q2 + + +`; + +exports[`Testing _qubitRegister register with small width 1`] = ` + + + + q0 + + +`; + +exports[`Testing _qubitRegister register with small width 2`] = ` + + + + q1 + + +`; + +exports[`Testing _qubitRegister register with small width 3`] = ` + + + + q2 + + +`; + +exports[`Testing formatRegisters 1 quantum register 1`] = ` + + + + + q0 + + + +`; + +exports[`Testing formatRegisters 1 quantum register 2`] = ` + + + + + q0 + + + +`; + +exports[`Testing formatRegisters 1 quantum register 3`] = ` + + + + + q0 + + + +`; + +exports[`Testing formatRegisters Multiple quantum registers 1`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + q3 + + + +`; + +exports[`Testing formatRegisters Multiple quantum registers 2`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + q3 + + + +`; + +exports[`Testing formatRegisters Multiple quantum registers 3`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + q3 + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 1`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 2`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 3`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 4`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 5`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`Testing formatRegisters Quantum and classical registers 6`] = ` + + + + + q0 + + + + + + q1 + + + + + + q2 + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`Testing formatRegisters Skipped quantum registers 1`] = ` + + + + + q0 + + + + + + q2 + + + + + + q3 + + + +`; + +exports[`Testing formatRegisters Skipped quantum registers 2`] = ` + + + + + q0 + + + + + + q2 + + + + + + q3 + + + +`; + +exports[`Testing formatRegisters Skipped quantum registers 3`] = ` + + + + + q0 + + + + + + q2 + + + + + + q3 + + + +`; diff --git a/circuit_vis/__tests__/gateFormatter.test.ts b/circuit_vis/__tests__/gateFormatter.test.ts new file mode 100644 index 0000000000..d0448c39f0 --- /dev/null +++ b/circuit_vis/__tests__/gateFormatter.test.ts @@ -0,0 +1,1061 @@ +import { + formatGates, + _formatGate, + _createGate, + _measure, + _unitary, + _swap, + _controlledGate, + _groupedOperations, + _classicalControlled, + _zoomButton, +} from "../src/formatters/gateFormatter"; +import { createSvgElement } from "../src/formatters/formatUtils"; +import { Metadata, GateType } from "../src/metadata"; +import { + startX, + startY, + registerHeight, + minGateWidth, + gatePadding, + classicalRegHeight, + controlBtnOffset, + groupBoxPadding, +} from "../src/constants"; + +describe("Testing _classicalControlled", () => { + test("one 'zero' child", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + classicalRegHeight], + targetsY: [startY], + width: minGateWidth + controlBtnOffset + groupBoxPadding * 2, + label: "", + children: [ + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + label: "X", + controlsY: [], + targetsY: [[startY]], + width: minGateWidth, + }, + ], + [], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("one 'one' child", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + classicalRegHeight], + targetsY: [startY], + width: minGateWidth + controlBtnOffset + groupBoxPadding * 2, + label: "", + children: [ + [], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + label: "X", + controlsY: [], + targetsY: [[startY]], + width: minGateWidth, + }, + ], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("one 'zero'/'one' child", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + classicalRegHeight], + targetsY: [startY, startY + classicalRegHeight * 2], + label: "", + width: minGateWidth + controlBtnOffset + groupBoxPadding * 2, + children: [ + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + ], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + controlsY: [], + targetsY: [[startY + classicalRegHeight * 2]], + label: "H", + width: minGateWidth, + }, + ], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("multiple 'zero'/'one' children", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + classicalRegHeight], + targetsY: [startY, startY + classicalRegHeight * 2], + label: "", + width: + minGateWidth * 2 + + gatePadding * 2 + + controlBtnOffset + + groupBoxPadding * 2, + children: [ + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: + startX + + minGateWidth + + minGateWidth / 2 + + gatePadding * 2 + + controlBtnOffset + + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "Z", + width: minGateWidth, + }, + ], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + controlBtnOffset + groupBoxPadding, + controlsY: [], + targetsY: [[startY + classicalRegHeight * 2]], + label: "H", + width: minGateWidth, + }, + ], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("nested children", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + registerHeight * 2], + targetsY: [startY, startY + registerHeight], + width: minGateWidth * 2 + gatePadding * 6, + label: "if", + children: [ + [], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + gatePadding, + label: "X", + controlsY: [], + targetsY: [[startY]], + width: minGateWidth, + }, + { + type: GateType.ClassicalControlled, + x: startX + minGateWidth + gatePadding * 3, + controlsY: [startY + registerHeight * 3], + targetsY: [startY, startY + registerHeight], + width: minGateWidth + gatePadding * 2, + label: "if", + children: [ + [], + [ + { + type: GateType.Cnot, + x: startX + minGateWidth + gatePadding * 4 + minGateWidth / 2, + label: "X", + controlsY: [startY + registerHeight], + targetsY: [startY], + width: minGateWidth, + }, + ], + ], + }, + ], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("No htmlClass", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + registerHeight * 2], + targetsY: [startY, startY + registerHeight], + width: minGateWidth * 2 + gatePadding * 4, + label: "if", + children: [ + [], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + gatePadding, + label: "X", + controlsY: [], + targetsY: [[startY]], + width: minGateWidth, + }, + ], + ], + }; + expect(_classicalControlled(metadata)).toMatchSnapshot(); + }); + test("change padding", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + registerHeight * 2], + targetsY: [startY, startY + registerHeight], + width: minGateWidth * 2 + gatePadding * 4, + label: "if", + children: [ + [], + [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + gatePadding, + label: "X", + controlsY: [], + targetsY: [[startY]], + width: minGateWidth, + }, + ], + ], + }; + expect(_classicalControlled(metadata, 20)).toMatchSnapshot(); + }); +}); + +describe("Testing _groupedOperations", () => { + test("one child", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY], + label: "", + width: minGateWidth + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); + test("children on consecutive registers", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY, startY + registerHeight], + label: "", + width: minGateWidth + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY + registerHeight]], + label: "H", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); + test("children on non-consecutive registers", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY, startY + registerHeight * 2], + label: "", + width: minGateWidth + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY + registerHeight * 2]], + label: "H", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); + test("children on same register", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY], + label: "", + width: minGateWidth * 2 + gatePadding * 2 + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: + startX + + minGateWidth + + minGateWidth / 2 + + gatePadding * 2 + + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "Z", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); + test("multiple children", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY, startY + registerHeight], + label: "", + width: minGateWidth * 2 + gatePadding * 2 + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: + startX + + minGateWidth + + minGateWidth / 2 + + gatePadding * 2 + + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "Z", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY + registerHeight]], + label: "H", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); + test("nested children", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY, startY + registerHeight], + label: "", + width: minGateWidth * 2 + gatePadding * 2 + groupBoxPadding * 4, + children: [ + { + type: GateType.Group, + x: startX + gatePadding, + controlsY: [], + targetsY: [startY], + label: "", + width: minGateWidth * 2 + gatePadding * 2 + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding * 2, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: + startX + + minGateWidth + + minGateWidth / 2 + + gatePadding * 2 + + groupBoxPadding * 2, + controlsY: [], + targetsY: [[startY]], + label: "Z", + width: minGateWidth, + }, + ], + }, + { + type: GateType.Unitary, + x: startX + minGateWidth + gatePadding + groupBoxPadding * 2, + controlsY: [], + targetsY: [[startY + registerHeight]], + label: "H", + width: minGateWidth, + }, + ], + }; + expect(_groupedOperations(metadata, 0)).toMatchSnapshot(); + }); +}); + +describe("Testing _controlledGate", () => { + test("CNOT gate", () => { + const metadata: Metadata = { + type: GateType.Cnot, + label: "X", + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + width: minGateWidth, + }; + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Flip target and control + metadata.controlsY = [startY + registerHeight]; + metadata.targetsY = [startY]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("SWAP gate", () => { + const metadata: Metadata = { + type: GateType.Swap, + label: "", + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight, startY + registerHeight * 2], + width: minGateWidth, + }; + // Control on top + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Control on bottom + metadata.controlsY = [startY + registerHeight * 2]; + metadata.targetsY = [startY, startY + registerHeight]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Control in middle + metadata.controlsY = [startY + registerHeight]; + metadata.targetsY = [startY, startY + registerHeight * 2]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Controlled U gate with 1 control + 1 target", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + label: "Foo", + x: startX, + controlsY: [startY], + targetsY: [[startY + registerHeight]], + width: 45, + }; + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Flip target and control + metadata.controlsY = [startY + registerHeight]; + metadata.targetsY = [[startY]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Controlled U gate with multiple controls + 1 target", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + label: "Foo", + x: startX, + controlsY: [startY, startY + registerHeight], + targetsY: [[startY + registerHeight * 2]], + width: 45, + }; + // Target on bottom + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Target on top + metadata.controlsY = [startY + registerHeight, startY + registerHeight * 2]; + metadata.targetsY = [[startY]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Target in middle + metadata.controlsY = [startY, startY + registerHeight * 2]; + metadata.targetsY = [[startY + registerHeight]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Controlled U gate with 1 control + 2 targets", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + label: "Foo", + x: startX, + controlsY: [startY + registerHeight * 2], + targetsY: [[startY, startY + registerHeight]], + width: 45, + }; + // Control on bottom + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Control on top + metadata.controlsY = [startY]; + metadata.targetsY = [ + [startY + registerHeight, startY + registerHeight * 2], + ]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Control in middle + metadata.controlsY = [startY + registerHeight]; + metadata.targetsY = [[startY], [startY + registerHeight * 2]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Controlled U gate with 2 controls + 2 targets", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + label: "Foo", + x: startX, + controlsY: [startY + registerHeight * 2, startY + registerHeight * 3], + targetsY: [[startY, startY + registerHeight]], + width: 45, + }; + // Controls on bottom + let svg: SVGElement = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Controls on top + metadata.controlsY = [startY, startY + registerHeight]; + metadata.targetsY = [ + [startY + registerHeight * 2, startY + registerHeight * 3], + ]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Controls in middle + metadata.controlsY = [startY + registerHeight, startY + registerHeight * 2]; + metadata.targetsY = [[startY], [startY + registerHeight * 3]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + + // Interleaved controls/targets + metadata.controlsY = [startY + registerHeight, startY + registerHeight * 3]; + metadata.targetsY = [[startY], [startY + registerHeight * 2]]; + svg = _controlledGate(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Invalid gate", () => { + const metadata: Metadata = { + type: GateType.Measure, + label: "X", + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + width: minGateWidth, + }; + expect(() => _controlledGate(metadata, 0)).toThrowError( + `ERROR: Unrecognized gate: X of type ${GateType.Measure}`, + ); + }); +}); + +describe("Testing _swap", () => { + const metadata: Metadata = { + type: GateType.Measure, + label: "SWAP", + x: startX, + controlsY: [], + targetsY: [startY + registerHeight], + width: minGateWidth, + }; + + test("Adjacent swap", () => { + metadata.targetsY = [startY, startY + registerHeight]; + let svg: SVGElement = _swap(metadata, 0); + expect(svg).toMatchSnapshot(); + // Flip target and control + metadata.targetsY = [startY + registerHeight, startY]; + svg = _swap(metadata, 0); + expect(svg).toMatchSnapshot(); + }); + test("Non-adjacent swap", () => { + metadata.targetsY = [startY, startY + registerHeight * 2]; + let svg: SVGElement = _swap(metadata, 0); + expect(svg).toMatchSnapshot(); + // Flip target and control + metadata.targetsY = [startY + registerHeight * 2, startY]; + svg = _swap(metadata, 0); + expect(svg).toMatchSnapshot(); + }); +}); + +describe("Testing _unitary", () => { + test("Single qubit unitary", () => { + expect(_unitary("H", startX, [[startY]], minGateWidth)).toMatchSnapshot(); + }); + test("Multiqubit unitary on consecutive registers", () => { + let svg: SVGElement = _unitary( + "ZZ", + startX, + [[startY, startY + registerHeight]], + minGateWidth, + ); + expect(svg).toMatchSnapshot(); + svg = _unitary( + "ZZZ", + startX, + [[startY, startY + registerHeight, startY + registerHeight * 2]], + minGateWidth, + ); + expect(svg).toMatchSnapshot(); + }); + test("Multiqubit unitary on non-consecutive registers", () => { + // Dashed line between unitaries + let svg: SVGElement = _unitary( + "ZZ", + startX, + [[startY], [startY + registerHeight * 2]], + minGateWidth, + ); + expect(svg).toMatchSnapshot(); + svg = _unitary( + "ZZZ", + startX, + [[startY], [startY + registerHeight * 2, startY + registerHeight * 3]], + minGateWidth, + ); + expect(svg).toMatchSnapshot(); + // Solid line + svg = _unitary( + "ZZ", + startX, + [[startY], [startY + registerHeight * 2]], + minGateWidth, + undefined, + false, + ); + expect(svg).toMatchSnapshot(); + svg = _unitary( + "ZZZ", + startX, + [[startY], [startY + registerHeight * 2, startY + registerHeight * 3]], + minGateWidth, + undefined, + false, + ); + expect(svg).toMatchSnapshot(); + }); + test("No y coords", () => { + expect(() => _unitary("ZZ", startX, [], minGateWidth)).toThrowError( + "Failed to render unitary gate (ZZ): has no y-values", + ); + }); +}); + +describe("Testing _measure", () => { + test("1 qubit + 1 classical registers", () => { + expect(_measure(startX, startY)).toMatchSnapshot(); + }); + test("2 qubit + 1 classical registers", () => { + expect(_measure(startX, startY)).toMatchSnapshot(); + }); + test("2 qubit + 2 classical registers", () => { + expect(_measure(startX, startY)).toMatchSnapshot(); + expect(_measure(startX, startY + registerHeight)).toMatchSnapshot(); + }); +}); + +describe("Testing _createGate", () => { + const metadata: Metadata = { + type: GateType.Invalid, + x: 0, + controlsY: [], + targetsY: [], + label: "", + width: -1, + dataAttributes: { a: "1", b: "2" }, + }; + const line: SVGElement = createSvgElement("line"); + test("Empty gate", () => { + expect(_createGate([line], metadata, 0).outerHTML).toEqual( + '', + ); + }); + test("Expanded gate", () => { + if (metadata.dataAttributes) metadata.dataAttributes["expanded"] = "true"; + expect(_createGate([line], metadata, 0)).toMatchSnapshot(); + }); +}); + +describe("Testing _zoomButton", () => { + const metadata: Metadata = { + type: GateType.Group, + x: startX, + controlsY: [], + targetsY: [startY], + label: "", + width: minGateWidth + groupBoxPadding * 2, + children: [ + { + type: GateType.Unitary, + x: startX + minGateWidth / 2 + groupBoxPadding, + controlsY: [], + targetsY: [[startY]], + label: "X", + width: minGateWidth, + }, + ], + }; + + test("Expanded gate", () => { + if (metadata.dataAttributes) { + metadata.dataAttributes["expanded"] = "true"; + metadata.dataAttributes["zoom-in"] = "true"; + } + expect(_zoomButton(metadata, 0)).toMatchSnapshot(); + }); + test("Non-expanded with no children gate", () => { + if (metadata.dataAttributes) { + delete metadata.dataAttributes["expanded"]; + delete metadata.dataAttributes["zoom-in"]; + } + expect(_zoomButton(metadata, 0)).toMatchSnapshot(); + }); + test("Non-expanded with children gate", () => { + if (metadata.dataAttributes) { + delete metadata.dataAttributes["expanded"]; + metadata.dataAttributes["zoom-in"] = "true"; + } + expect(_zoomButton(metadata, 0)).toMatchSnapshot(); + }); + test("Expanded with children gate", () => { + if (metadata.dataAttributes) { + metadata.dataAttributes["expanded"] = "true"; + metadata.dataAttributes["zoom-in"] = "true"; + } + expect(_zoomButton(metadata, 0)).toMatchSnapshot(); + }); +}); + +describe("Testing _formatGate", () => { + test("measure gate", () => { + const metadata: Metadata = { + type: GateType.Measure, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + label: "", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("single-qubit unitary gate", () => { + const metadata: Metadata = { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY]], + label: "H", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("single-qubit unitary gate with arguments", () => { + const metadata: Metadata = { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY]], + label: "Ry", + displayArgs: "(0.25)", + width: 52, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("multi-qubit unitary gate", () => { + const metadata: Metadata = { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY, startY + registerHeight]], + label: "U", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("multi-qubit unitary gate with arguments", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + x: startX, + controlsY: [], + targetsY: [[startY, startY + registerHeight]], + label: "U", + displayArgs: "('foo', 'bar')", + width: 77, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("swap gate", () => { + const metadata: Metadata = { + type: GateType.Swap, + x: startX, + controlsY: [], + targetsY: [startY, startY + registerHeight], + label: "", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("controlled swap gate", () => { + const metadata: Metadata = { + type: GateType.Swap, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight, startY + registerHeight * 2], + label: "", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("CNOT gate", () => { + const metadata: Metadata = { + type: GateType.Cnot, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + label: "X", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("controlled unitary gate", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + x: startX, + controlsY: [startY], + targetsY: [[startY + registerHeight]], + label: "U", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("controlled unitary gate with arguments", () => { + const metadata: Metadata = { + type: GateType.ControlledUnitary, + x: startX, + controlsY: [startY], + targetsY: [[startY + registerHeight]], + label: "U", + displayArgs: "('foo', 'bar')", + width: 77, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("classically controlled gate", () => { + const metadata: Metadata = { + type: GateType.ClassicalControlled, + x: startX, + controlsY: [startY + registerHeight * 2], + targetsY: [startY, startY + registerHeight], + label: "", + width: minGateWidth, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("gate with metadata", () => { + const metadata: Metadata = { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY]], + label: "H", + width: minGateWidth, + dataAttributes: { a: "1", b: "2" }, + }; + expect(_formatGate(metadata)).toMatchSnapshot(); + }); + test("invalid gate", () => { + const metadata: Metadata = { + type: GateType.Invalid, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + label: "Foo", + width: 48, + }; + expect(() => _formatGate(metadata)).toThrowError( + `ERROR: unknown gate (Foo) of type ${GateType.Invalid}.`, + ); + }); +}); + +describe("Testing formatGates", () => { + test("Single gate", () => { + const gates: Metadata[] = [ + { + type: GateType.Cnot, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + label: "X", + width: minGateWidth, + }, + ]; + expect(formatGates(gates)).toMatchSnapshot(); + }); + test("Single null gate", () => { + const gates: Metadata[] = [ + { + type: GateType.Invalid, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight], + label: "", + width: minGateWidth, + }, + ]; + expect(() => formatGates(gates)).toThrowError( + `ERROR: unknown gate () of type ${GateType.Invalid}.`, + ); + }); + test("Multiple gates", () => { + const gates: Metadata[] = [ + { + type: GateType.Cnot, + x: startX, + controlsY: [startY + registerHeight], + targetsY: [startY], + label: "X", + width: minGateWidth, + }, + { + type: GateType.ControlledUnitary, + x: startX, + controlsY: [startY + registerHeight], + targetsY: [[startY + registerHeight * 2]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY + registerHeight * 2]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Measure, + x: startX, + controlsY: [startY], + targetsY: [startY + registerHeight * 3], + label: "X", + width: minGateWidth, + }, + ]; + expect(formatGates(gates)).toMatchSnapshot(); + }); + test("Multiple gates with invalid gate", () => { + const gates: Metadata[] = [ + { + type: GateType.Unitary, + x: startX, + controlsY: [], + targetsY: [[startY + registerHeight * 2]], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Cnot, + x: startX, + controlsY: [startY + registerHeight], + targetsY: [startY], + label: "X", + width: minGateWidth, + }, + { + type: GateType.Invalid, + x: startX, + controlsY: [], + targetsY: [startY + registerHeight * 2], + label: "", + width: minGateWidth, + }, + { + type: GateType.Invalid, + x: startX, + controlsY: [], + targetsY: [], + label: "", + width: minGateWidth, + }, + ]; + expect(() => formatGates(gates)).toThrowError( + `ERROR: unknown gate () of type ${GateType.Invalid}.`, + ); + }); +}); diff --git a/circuit_vis/__tests__/inputFormatter.test.ts b/circuit_vis/__tests__/inputFormatter.test.ts new file mode 100644 index 0000000000..da37a1b8f5 --- /dev/null +++ b/circuit_vis/__tests__/inputFormatter.test.ts @@ -0,0 +1,102 @@ +import { Qubit } from "../src/circuit"; +import { RegisterMap, RegisterType } from "../src/register"; +import { formatInputs, _qubitInput } from "../src/formatters/inputFormatter"; +import { startY, registerHeight, classicalRegHeight } from "../src/constants"; + +describe("Testing _qubitInput", () => { + test("classical register", () => { + expect(_qubitInput(20)).toMatchSnapshot(); + expect(_qubitInput(50)).toMatchSnapshot(); + expect(_qubitInput(0)).toMatchSnapshot(); + }); +}); + +describe("Testing formatInputs", () => { + test("1 quantum register", () => { + const inputs: Qubit[] = [{ id: 0 }]; + const expectedRegs: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + }; + const { qubitWires, registers } = formatInputs(inputs); + expect(qubitWires).toMatchSnapshot(); + expect(registers).toEqual(expectedRegs); + expect(registers).toEqual(expectedRegs); + }); + test("Multiple quantum registers", () => { + const inputs: Qubit[] = [{ id: 0 }, { id: 1 }, { id: 2 }]; + const expectedRegs: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + 1: { type: RegisterType.Qubit, y: startY + registerHeight }, + 2: { type: RegisterType.Qubit, y: startY + registerHeight * 2 }, + }; + const { qubitWires, registers } = formatInputs(inputs); + expect(qubitWires).toMatchSnapshot(); + expect(registers).toEqual(expectedRegs); + expect(registers).toEqual(expectedRegs); + }); + test("Quantum and classical registers", () => { + let inputs: Qubit[] = [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }]; + let expectedRegs: RegisterMap = { + 0: { + type: RegisterType.Qubit, + y: startY, + children: [ + { type: RegisterType.Classical, y: startY + classicalRegHeight }, + ], + }, + 1: { type: RegisterType.Qubit, y: startY + classicalRegHeight * 2 }, + 2: { + type: RegisterType.Qubit, + y: startY + registerHeight + classicalRegHeight * 2, + }, + }; + let { qubitWires, registers } = formatInputs(inputs); + expect(qubitWires).toMatchSnapshot(); + expect(registers).toEqual(expectedRegs); + expect(registers).toEqual(expectedRegs); + + inputs = [{ id: 0 }, { id: 1, numChildren: 2 }, { id: 2, numChildren: 1 }]; + expectedRegs = { + 0: { type: RegisterType.Qubit, y: startY }, + 1: { + type: RegisterType.Qubit, + y: startY + registerHeight, + children: [ + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight, + }, + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight * 2, + }, + ], + }, + 2: { + type: RegisterType.Qubit, + y: startY + registerHeight + classicalRegHeight * 3, + children: [ + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight * 4, + }, + ], + }, + }; + ({ qubitWires, registers } = formatInputs(inputs)); + expect(qubitWires).toMatchSnapshot(); + expect(registers).toEqual(expectedRegs); + expect(registers).toEqual(expectedRegs); + }); + test("Skip quantum registers", () => { + const inputs: Qubit[] = [{ id: 0 }, { id: 2 }]; + const expectedRegs: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + 2: { type: RegisterType.Qubit, y: startY + registerHeight }, + }; + const { qubitWires, registers } = formatInputs(inputs); + expect(qubitWires).toMatchSnapshot(); + expect(registers).toEqual(expectedRegs); + expect(registers).toEqual(expectedRegs); + }); +}); diff --git a/circuit_vis/__tests__/registerFormatter.test.ts b/circuit_vis/__tests__/registerFormatter.test.ts new file mode 100644 index 0000000000..1fec69e603 --- /dev/null +++ b/circuit_vis/__tests__/registerFormatter.test.ts @@ -0,0 +1,205 @@ +import { + formatRegisters, + _classicalRegister, + _qubitRegister, +} from "../src/formatters/registerFormatter"; +import { RegisterMap, RegisterType } from "../src/register"; +import { Metadata, GateType } from "../src/metadata"; +import { + startY, + registerHeight, + classicalRegHeight, + startX, + minGateWidth, +} from "../src/constants"; + +describe("Testing _classicalRegister", () => { + test("register with normal width", () => { + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + }); + test("register with small width", () => { + expect(_classicalRegister(10, 10, 5, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 5, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 5, 20)).toMatchSnapshot(); + }); + test("register with large width", () => { + expect(_classicalRegister(10, 10, 500, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 500, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 500, 20)).toMatchSnapshot(); + }); + test("register with label offset", () => { + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + expect(_classicalRegister(10, 10, 100, 20)).toMatchSnapshot(); + }); +}); + +describe("Testing _qubitRegister", () => { + test("register with normal width", () => { + expect(_qubitRegister(0, 100, 20)).toMatchSnapshot(); + expect(_qubitRegister(1, 100, 20)).toMatchSnapshot(); + expect(_qubitRegister(2, 100, 20)).toMatchSnapshot(); + }); + test("register with small width", () => { + expect(_qubitRegister(0, 5, 20)).toMatchSnapshot(); + expect(_qubitRegister(1, 5, 20)).toMatchSnapshot(); + expect(_qubitRegister(2, 5, 20)).toMatchSnapshot(); + }); + test("register with large width", () => { + expect(_qubitRegister(0, 500, 20)).toMatchSnapshot(); + expect(_qubitRegister(1, 500, 20)).toMatchSnapshot(); + expect(_qubitRegister(2, 500, 20)).toMatchSnapshot(); + }); + test("register with label offset", () => { + expect(_qubitRegister(0, 100, 20, 0)).toMatchSnapshot(); + expect(_qubitRegister(1, 100, 20, 5)).toMatchSnapshot(); + expect(_qubitRegister(2, 100, 20, 50)).toMatchSnapshot(); + }); +}); + +describe("Testing formatRegisters", () => { + test("1 quantum register", () => { + const registers: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + }; + // Normal width + expect(formatRegisters(registers, [], startX + 100)).toMatchSnapshot(); + // Small width + expect(formatRegisters(registers, [], startX + 5)).toMatchSnapshot(); + // Large width + expect(formatRegisters(registers, [], startX + 500)).toMatchSnapshot(); + }); + test("Multiple quantum registers", () => { + const registers: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + 1: { type: RegisterType.Qubit, y: startY + registerHeight }, + 2: { type: RegisterType.Qubit, y: startY + registerHeight * 2 }, + 3: { type: RegisterType.Qubit, y: startY + registerHeight * 3 }, + }; + // Normal width + expect(formatRegisters(registers, [], startX + 100)).toMatchSnapshot(); + // Small width + expect(formatRegisters(registers, [], startX + 5)).toMatchSnapshot(); + // Large width + expect(formatRegisters(registers, [], startX + 500)).toMatchSnapshot(); + }); + test("Skipped quantum registers", () => { + const registers: RegisterMap = { + 0: { type: RegisterType.Qubit, y: startY }, + 2: { type: RegisterType.Qubit, y: startY + registerHeight * 2 }, + 3: { type: RegisterType.Qubit, y: startY + registerHeight * 3 }, + }; + // Normal width + expect(formatRegisters(registers, [], startX + 100)).toMatchSnapshot(); + // Small width + expect(formatRegisters(registers, [], startX + 5)).toMatchSnapshot(); + // Large width + expect(formatRegisters(registers, [], startX + 500)).toMatchSnapshot(); + }); + test("Quantum and classical registers", () => { + let registers: RegisterMap = { + 0: { + type: RegisterType.Qubit, + y: startY, + children: [ + { type: RegisterType.Classical, y: startY + classicalRegHeight }, + ], + }, + 1: { type: RegisterType.Qubit, y: startY + classicalRegHeight * 2 }, + 2: { + type: RegisterType.Qubit, + y: startY + registerHeight + classicalRegHeight * 2, + }, + }; + let measureGates: Metadata[] = [ + { + type: GateType.Measure, + x: startX, + controlsY: [startY], + targetsY: [startY + classicalRegHeight], + label: "", + width: minGateWidth, + }, + ]; + // Normal width + expect( + formatRegisters(registers, measureGates, startX + 100), + ).toMatchSnapshot(); + // Small width + expect( + formatRegisters(registers, measureGates, startX + 5), + ).toMatchSnapshot(); + // Large width + expect( + formatRegisters(registers, measureGates, startX + 500), + ).toMatchSnapshot(); + + registers = { + 0: { type: RegisterType.Qubit, y: startY }, + 1: { + type: RegisterType.Qubit, + y: startY + registerHeight, + children: [ + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight, + }, + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight * 2, + }, + ], + }, + 2: { + type: RegisterType.Qubit, + y: startY + registerHeight + classicalRegHeight * 3, + children: [ + { + type: RegisterType.Classical, + y: startY + registerHeight + classicalRegHeight * 4, + }, + ], + }, + }; + measureGates = [ + { + type: GateType.Measure, + x: startX, + controlsY: [startY], + targetsY: [startY + classicalRegHeight], + label: "", + width: minGateWidth, + }, + { + type: GateType.Measure, + x: startX, + controlsY: [startY], + targetsY: [startY + classicalRegHeight * 2], + label: "", + width: minGateWidth, + }, + { + type: GateType.Measure, + x: startX, + controlsY: [startY + classicalRegHeight * 3], + targetsY: [startY + classicalRegHeight * 4], + label: "", + width: minGateWidth, + }, + ]; + // Normal width + expect( + formatRegisters(registers, measureGates, startX + 100), + ).toMatchSnapshot(); + // Small width + expect( + formatRegisters(registers, measureGates, startX + 5), + ).toMatchSnapshot(); + // Large width + expect( + formatRegisters(registers, measureGates, startX + 500), + ).toMatchSnapshot(); + }); +}); diff --git a/circuit_vis/jest.config.js b/circuit_vis/jest.config.js new file mode 100644 index 0000000000..49bd84ae2d --- /dev/null +++ b/circuit_vis/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + roots: [""], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", + moduleFileExtensions: ["ts", "js"], +}; diff --git a/circuit_vis/package.json b/circuit_vis/package.json new file mode 100644 index 0000000000..392e933e94 --- /dev/null +++ b/circuit_vis/package.json @@ -0,0 +1,32 @@ +{ + "name": "@microsoft/quantum-viz.js", + "version": "1.0.6", + "description": "quantum-viz.js is a configurable tool for visualizing quantum circuits.", + "main": "dist/qviz.min.js", + "scripts": { + "build": "tsc", + "build:prod": "tsc && webpack", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "prepublishOnly": "npm run lint && npm run test && npm run build:prod", + "start": "tsc -w" + }, + "files": [ + "README.md", + "LICENSE.txt", + "lib", + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/quantum-viz.js" + }, + "keywords": [ + "quantum", + "circuits", + "visualization" + ], + "author": "Microsoft Quantum", + "license": "MIT", + "homepage": "https://github.com/microsoft/quantum-viz.js#readme" +} diff --git a/circuit_vis/src/circuit.ts b/circuit_vis/src/circuit.ts new file mode 100644 index 0000000000..584069a785 --- /dev/null +++ b/circuit_vis/src/circuit.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Register } from "./register"; + +/** + * Circuit to be visualized. + */ +export interface Circuit { + /** Array of qubit resources. */ + qubits: Qubit[]; + operations: Operation[]; +} + +/** + * Represents a unique qubit resource bit. + */ +export interface Qubit { + /** Qubit ID. */ + id: number; + /** Number of classical registers attached to quantum register. */ + numChildren?: number; +} + +/** + * Conditions on when to render the given operation. + */ +export enum ConditionalRender { + /** Always rendered. */ + Always, + /** Render classically-controlled operation when measurement is a zero. */ + OnZero, + /** Render classically-controlled operation when measurement is a one. */ + OnOne, + /** Render operation as a group of its nested operations. */ + AsGroup, +} + +/** + * Custom data attributes (e.g. data-{attr}="{val}") + */ +export interface DataAttributes { + [attr: string]: string; +} + +/** + * Represents an operation and the registers it acts on. + */ +export interface Operation { + /** Gate label. */ + gate: string; + /** Formatted gate arguments to be displayed. */ + displayArgs?: string; + /** Nested operations within this operation. */ + children?: Operation[]; + /** Whether gate is a measurement operation. */ + isMeasurement: boolean; + /** Whether gate is a conditional operation. */ + isConditional: boolean; + /** Whether gate is a controlled operation. */ + isControlled: boolean; + /** Whether gate is an adjoint operation. */ + isAdjoint: boolean; + /** Control registers the gate acts on. */ + controls?: Register[]; + /** Target registers the gate acts on. */ + targets: Register[]; + /** Specify conditions on when to render operation. */ + conditionalRender?: ConditionalRender; + /** Custom data attributes to attach to gate element. */ + dataAttributes?: DataAttributes; +} diff --git a/circuit_vis/src/constants.ts b/circuit_vis/src/constants.ts new file mode 100644 index 0000000000..fe2763319c --- /dev/null +++ b/circuit_vis/src/constants.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// SVG Namespace +export const svgNS = "http://www.w3.org/2000/svg"; + +// Display attributes +/** Left padding of SVG. */ +export const leftPadding = 20; +/** x coordinate for first operation on each register. */ +export const startX = 80; +/** y coordinate of first register. */ +export const startY = 40; +/** Minimum width of each gate. */ +export const minGateWidth = 40; +/** Height of each gate. */ +export const gateHeight = 40; +/** Padding on each side of gate. */ +export const gatePadding = 10; +/** Padding on each side of gate label. */ +export const labelPadding = 10; +/** Height between each qubit register. */ +export const registerHeight: number = gateHeight + gatePadding * 2; +/** Height between classical registers. */ +export const classicalRegHeight: number = gateHeight; +/** Group box inner padding. */ +export const groupBoxPadding = gatePadding; +/** Padding between nested groups. */ +export const nestedGroupPadding = 2; +/** Additional offset for control button. */ +export const controlBtnOffset = 40; +/** Control button radius. */ +export const controlBtnRadius = 15; +/** Default font size for gate labels. */ +export const labelFontSize = 14; +/** Default font size for gate arguments. */ +export const argsFontSize = 12; +/** Starting x coord for each register wire. */ +export const regLineStart = 40; diff --git a/circuit_vis/src/formatters/formatUtils.ts b/circuit_vis/src/formatters/formatUtils.ts new file mode 100644 index 0000000000..c3bccd1beb --- /dev/null +++ b/circuit_vis/src/formatters/formatUtils.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { labelFontSize, svgNS } from "../constants"; + +// Helper functions for basic SVG components + +/** + * Create an SVG element. + * + * @param type The type of element to be created. + * @param attributes The attributes that define the element. + * + * @returns SVG element. + */ +export const createSvgElement = ( + type: string, + attributes: { [attr: string]: string } = {}, +): SVGElement => { + const el: SVGElement = document.createElementNS(svgNS, type); + Object.entries(attributes).forEach(([attrName, attrVal]) => + el.setAttribute(attrName, attrVal), + ); + return el; +}; + +/** + * Given an array of SVG elements, group them as an SVG group using the `` tag. + * + * @param svgElems Array of SVG elements. + * @param attributes Key-value pairs of attributes and they values. + * + * @returns SVG element for grouped elements. + */ +export const group = ( + svgElems: SVGElement[], + attributes: { [attr: string]: string } = {}, +): SVGElement => { + const el: SVGElement = createSvgElement("g", attributes); + svgElems.forEach((child: SVGElement) => el.appendChild(child)); + return el; +}; + +/** + * Generate an SVG line. + * + * @param x1 x coord of starting point of line. + * @param y1 y coord of starting point of line. + * @param x2 x coord of ending point of line. + * @param y2 y coord fo ending point of line. + * @param className Class name of element. + * + * @returns SVG element for line. + */ +export const line = ( + x1: number, + y1: number, + x2: number, + y2: number, + className?: string, +): SVGElement => { + const attrs: { [attr: string]: string } = { + x1: x1.toString(), + x2: x2.toString(), + y1: y1.toString(), + y2: y2.toString(), + }; + if (className != null) attrs["class"] = className; + return createSvgElement("line", attrs); +}; + +/** + * Generate an SVG circle. + * + * @param x x coord of circle. + * @param y y coord of circle. + * @param radius Radius of circle. + * + * @returns SVG element for circle. + */ +export const circle = ( + x: number, + y: number, + radius: number, + className?: string, +): SVGElement => { + const attrs: { [attr: string]: string } = { + cx: x.toString(), + cy: y.toString(), + r: radius.toString(), + }; + if (className != null) attrs["class"] = className; + return createSvgElement("circle", attrs); +}; + +/** + * Generate the SVG representation of a control dot used for controlled operations. + * + * @param x x coord of circle. + * @param y y coord of circle. + * @param radius Radius of circle. + * + * @returns SVG element for control dot. + */ +export const controlDot = (x: number, y: number, radius = 5): SVGElement => + circle(x, y, radius, "control-dot"); + +/** + * Generate the SVG representation of a unitary box that represents an arbitrary unitary operation. + * + * @param x x coord of box. + * @param y y coord of box. + * @param width Width of box. + * @param height Height of box. + * @param className Class name of element. + * + * @returns SVG element for unitary box. + */ +export const box = ( + x: number, + y: number, + width: number, + height: number, + className = "gate-unitary", +): SVGElement => + createSvgElement("rect", { + class: className, + x: x.toString(), + y: y.toString(), + width: width.toString(), + height: height.toString(), + }); + +/** + * Generate the SVG text element from a given text string. + * + * @param text String to render as SVG text. + * @param x Middle x coord of text. + * @param y Middle y coord of text. + * @param fs Font size of text. + * + * @returns SVG element for text. + */ +export const text = ( + text: string, + x: number, + y: number, + fs: number = labelFontSize, +): SVGElement => { + const el: SVGElement = createSvgElement("text", { + "font-size": fs.toString(), + x: x.toString(), + y: y.toString(), + }); + el.textContent = text; + return el; +}; + +/** + * Generate the SVG representation of the arc used in the measurement box. + * + * @param x x coord of arc. + * @param y y coord of arc. + * @param rx x radius of arc. + * @param ry y radius of arc. + * + * @returns SVG element for arc. + */ +export const arc = (x: number, y: number, rx: number, ry: number): SVGElement => + createSvgElement("path", { + class: "arc-measure", + d: `M ${x + 2 * rx} ${y} A ${rx} ${ry} 0 0 0 ${x} ${y}`, + }); + +/** + * Generate a dashed SVG line. + * + * @param x1 x coord of starting point of line. + * @param y1 y coord of starting point of line. + * @param x2 x coord of ending point of line. + * @param y2 y coord fo ending point of line. + * @param className Class name of element. + * + * @returns SVG element for dashed line. + */ +export const dashedLine = ( + x1: number, + y1: number, + x2: number, + y2: number, + className?: string, +): SVGElement => { + const el: SVGElement = line(x1, y1, x2, y2, className); + el.setAttribute("stroke-dasharray", "8, 8"); + return el; +}; + +/** + * Generate the SVG representation of the dashed box used for enclosing groups of operations controlled on a classical register. + * + * @param x x coord of box. + * @param y y coord of box. + * @param width Width of box. + * @param height Height of box. + * @param className Class name of element. + * + * @returns SVG element for dashed box. + */ +export const dashedBox = ( + x: number, + y: number, + width: number, + height: number, + className?: string, +): SVGElement => { + const el: SVGElement = box(x, y, width, height, className); + el.setAttribute("fill-opacity", "0"); + el.setAttribute("stroke-dasharray", "8, 8"); + return el; +}; diff --git a/circuit_vis/src/formatters/gateFormatter.ts b/circuit_vis/src/formatters/gateFormatter.ts new file mode 100644 index 0000000000..f4be3626df --- /dev/null +++ b/circuit_vis/src/formatters/gateFormatter.ts @@ -0,0 +1,557 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Metadata, GateType } from "../metadata"; +import { + minGateWidth, + gateHeight, + labelFontSize, + argsFontSize, + controlBtnRadius, + controlBtnOffset, + groupBoxPadding, + classicalRegHeight, + nestedGroupPadding, +} from "../constants"; +import { + createSvgElement, + group, + line, + circle, + controlDot, + box, + text, + arc, + dashedLine, + dashedBox, +} from "./formatUtils"; + +/** + * Given an array of operations (in metadata format), return the SVG representation. + * + * @param opsMetadata Array of Metadata representation of operations. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of operations. + */ +const formatGates = (opsMetadata: Metadata[], nestedDepth = 0): SVGElement => { + const formattedGates: SVGElement[] = opsMetadata.map((metadata) => + _formatGate(metadata, nestedDepth), + ); + return group(formattedGates); +}; + +/** + * Takes in an operation's metadata and formats it into SVG. + * + * @param metadata Metadata object representation of gate. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of gate. + */ +const _formatGate = (metadata: Metadata, nestedDepth = 0): SVGElement => { + const { type, x, controlsY, targetsY, label, displayArgs, width } = metadata; + switch (type) { + case GateType.Measure: + return _createGate([_measure(x, controlsY[0])], metadata, nestedDepth); + case GateType.Unitary: + return _createGate( + [_unitary(label, x, targetsY as number[][], width, displayArgs)], + metadata, + nestedDepth, + ); + case GateType.X: + return _createGate([_x(metadata, nestedDepth)], metadata, nestedDepth); + case GateType.Swap: + return controlsY.length > 0 + ? _controlledGate(metadata, nestedDepth) + : _createGate([_swap(metadata, nestedDepth)], metadata, nestedDepth); + case GateType.Cnot: + case GateType.ControlledUnitary: + return _controlledGate(metadata, nestedDepth); + case GateType.Group: + return _groupedOperations(metadata, nestedDepth); + case GateType.ClassicalControlled: + return _classicalControlled(metadata); + default: + throw new Error(`ERROR: unknown gate (${label}) of type ${type}.`); + } +}; + +/** + * Groups SVG elements into a gate SVG group. + * + * @param svgElems Array of SVG elements. + * @param dataAttributes Custom data attributes to be attached to SVG group. + * + * @returns SVG representation of a gate. + */ +const _createGate = ( + svgElems: SVGElement[], + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const { dataAttributes } = metadata || {}; + const attributes: { [attr: string]: string } = { class: "gate" }; + Object.entries(dataAttributes || {}).forEach( + ([attr, val]) => (attributes[`data-${attr}`] = val), + ); + + const zoomBtn: SVGElement | null = _zoomButton(metadata, nestedDepth); + if (zoomBtn != null) svgElems = svgElems.concat([zoomBtn]); + return group(svgElems, attributes); +}; + +/** + * Returns the expand/collapse button for an operation if it can be zoomed-in or zoomed-out, + * respectively. If neither are allowed, return `null`. + * + * @param metadata Operation metadata. + * @param nestedDepth Depth of nested operation. + * + * @returns SVG element for expand/collapse button if needed, or null otherwise. + */ +const _zoomButton = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement | null => { + if (metadata == undefined) return null; + + const [x1, y1] = _gatePosition(metadata, nestedDepth); + let { dataAttributes } = metadata; + dataAttributes = dataAttributes || {}; + + const expanded = "expanded" in dataAttributes; + + const x = x1 + 2; + const y = y1 + 2; + const circleBorder: SVGElement = circle(x, y, 10); + + if (expanded) { + // Create collapse button if expanded + const minusSign: SVGElement = createSvgElement("path", { + d: `M${x - 7},${y} h14`, + }); + const elements: SVGElement[] = [circleBorder, minusSign]; + return group(elements, { class: "gate-control gate-collapse" }); + } else if (dataAttributes["zoom-in"] == "true") { + // Create expand button if operation can be zoomed in + const plusSign: SVGElement = createSvgElement("path", { + d: `M${x},${y - 7} v14 M${x - 7},${y} h14`, + }); + const elements: SVGElement[] = [circleBorder, plusSign]; + return group(elements, { class: "gate-control gate-expand" }); + } + + return null; +}; + +/** + * Calculate position of gate. + * + * @param metadata Operation metadata. + * @param nestedDepth Depth of nested operations. + * + * @returns Coordinates of gate: [x1, y1, x2, y2]. + */ +const _gatePosition = ( + metadata: Metadata, + nestedDepth: number, +): [number, number, number, number] => { + const { x, width, type, targetsY } = metadata; + + const ys = targetsY?.flatMap((y) => y as number[]) || []; + const maxY = Math.max(...ys); + const minY = Math.min(...ys); + + let x1: number, y1: number, x2: number, y2: number; + + switch (type) { + case GateType.Group: { + const padding = groupBoxPadding - nestedDepth * nestedGroupPadding; + + x1 = x - 2 * padding; + y1 = minY - gateHeight / 2 - padding; + x2 = width + 2 * padding; + y2 = maxY + +gateHeight / 2 + padding - (minY - gateHeight / 2 - padding); + + return [x1, y1, x2, y2]; + } + + default: + x1 = x - width / 2; + y1 = minY - gateHeight / 2; + x2 = x + width; + y2 = maxY + gateHeight / 2; + } + + return [x1, y1, x2, y2]; +}; + +/** + * Creates a measurement gate at position (x, y). + * + * @param x x coord of measurement gate. + * @param y y coord of measurement gate. + * + * @returns SVG representation of measurement gate. + */ +const _measure = (x: number, y: number): SVGElement => { + x -= minGateWidth / 2; + const width: number = minGateWidth, + height = gateHeight; + // Draw measurement box + const mBox: SVGElement = box( + x, + y - height / 2, + width, + height, + "gate-measure", + ); + const mArc: SVGElement = arc(x + 5, y + 2, width / 2 - 5, height / 2 - 8); + const meter: SVGElement = line( + x + width / 2, + y + 8, + x + width - 8, + y - height / 2 + 8, + ); + return group([mBox, mArc, meter]); +}; + +/** + * Creates the SVG for a unitary gate on an arbitrary number of qubits. + * + * @param label Gate label. + * @param x x coord of gate. + * @param y Array of y coords of registers acted upon by gate. + * @param width Width of gate. + * @param displayArgs Arguments passed in to gate. + * @param renderDashedLine If true, draw dashed lines between non-adjacent unitaries. + * + * @returns SVG representation of unitary gate. + */ +const _unitary = ( + label: string, + x: number, + y: number[][], + width: number, + displayArgs?: string, + renderDashedLine = true, +): SVGElement => { + if (y.length === 0) + throw new Error( + `Failed to render unitary gate (${label}): has no y-values`, + ); + + // Render each group as a separate unitary boxes + const unitaryBoxes: SVGElement[] = y.map((group: number[]) => { + const maxY: number = group[group.length - 1], + minY: number = group[0]; + const height: number = maxY - minY + gateHeight; + return _unitaryBox(label, x, minY, width, height, displayArgs); + }); + + // Draw dashed line between disconnected unitaries + if (renderDashedLine && unitaryBoxes.length > 1) { + const lastBox: number[] = y[y.length - 1]; + const firstBox: number[] = y[0]; + const maxY: number = lastBox[lastBox.length - 1], + minY: number = firstBox[0]; + const vertLine: SVGElement = dashedLine(x, minY, x, maxY); + return group([vertLine, ...unitaryBoxes]); + } + + return group(unitaryBoxes); +}; + +/** + * Generates SVG representation of the boxed unitary gate symbol. + * + * @param label Label for unitary operation. + * @param x x coord of gate. + * @param y y coord of gate. + * @param width Width of gate. + * @param height Height of gate. + * @param displayArgs Arguments passed in to gate. + * + * @returns SVG representation of unitary box. + */ +const _unitaryBox = ( + label: string, + x: number, + y: number, + width: number, + height: number = gateHeight, + displayArgs?: string, +): SVGElement => { + y -= gateHeight / 2; + const uBox: SVGElement = box(x - width / 2, y, width, height); + const labelY = y + height / 2 - (displayArgs == null ? 0 : 7); + const labelText: SVGElement = text(label, x, labelY); + const elems = [uBox, labelText]; + if (displayArgs != null) { + const argStrY = y + height / 2 + 8; + const argText: SVGElement = text(displayArgs, x, argStrY, argsFontSize); + elems.push(argText); + } + return group(elems); +}; + +/** + * Creates the SVG for a SWAP gate on y coords given by targetsY. + * + * @param x Centre x coord of SWAP gate. + * @param targetsY y coords of target registers. + * + * @returns SVG representation of SWAP gate. + */ +const _swap = (metadata: Metadata, nestedDepth: number): SVGElement => { + const { x, targetsY } = metadata; + + // Get SVGs of crosses + const [x1, y1, x2, y2] = _gatePosition(metadata, nestedDepth); + const ys = targetsY?.flatMap((y) => y as number[]) || []; + + const bg: SVGElement = box(x1, y1, x2, y2, "gate-swap"); + const crosses: SVGElement[] = ys.map((y) => _cross(x, y)); + const vertLine: SVGElement = line(x, ys[0], x, ys[1]); + return group([bg, ...crosses, vertLine]); +}; +/** + * Creates the SVG for an X gate + * + * @returns SVG representation of X gate. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _x = (metadata: Metadata, _: number): SVGElement => { + const { x, targetsY } = metadata; + const ys = targetsY.flatMap((y) => y as number[]); + return _oplus(x, ys[0]); +}; +/** + * Generates cross for display in SWAP gate. + * + * @param x x coord of gate. + * @param y y coord of gate. + * + * @returns SVG representation for cross. + */ +const _cross = (x: number, y: number): SVGElement => { + const radius = 8; + const line1: SVGElement = line( + x - radius, + y - radius, + x + radius, + y + radius, + ); + const line2: SVGElement = line( + x - radius, + y + radius, + x + radius, + y - radius, + ); + return group([line1, line2]); +}; + +/** + * Produces the SVG representation of a controlled gate on multiple qubits. + * + * @param metadata Metadata of controlled gate. + * + * @returns SVG representation of controlled gate. + */ +const _controlledGate = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const targetGateSvgs: SVGElement[] = []; + const { type, x, controlsY, label, displayArgs, width } = metadata; + let { targetsY } = metadata; + + // Get SVG for target gates + switch (type) { + case GateType.Cnot: + (targetsY as number[]).forEach((y) => targetGateSvgs.push(_oplus(x, y))); + break; + case GateType.Swap: + (targetsY as number[]).forEach((y) => targetGateSvgs.push(_cross(x, y))); + break; + case GateType.ControlledUnitary: + { + const groupedTargetsY: number[][] = targetsY as number[][]; + targetGateSvgs.push( + _unitary(label, x, groupedTargetsY, width, displayArgs, false), + ); + targetsY = targetsY.flat(); + } + break; + default: + throw new Error(`ERROR: Unrecognized gate: ${label} of type ${type}`); + } + // Get SVGs for control dots + const controlledDotsSvg: SVGElement[] = controlsY.map((y) => + controlDot(x, y), + ); + // Create control lines + const maxY: number = Math.max(...controlsY, ...(targetsY as number[])); + const minY: number = Math.min(...controlsY, ...(targetsY as number[])); + const vertLine: SVGElement = line(x, minY, x, maxY); + const svg: SVGElement = _createGate( + [vertLine, ...controlledDotsSvg, ...targetGateSvgs], + metadata, + nestedDepth, + ); + return svg; +}; + +/** + * Generates $\oplus$ symbol for display in CNOT gate. + * + * @param x x coordinate of gate. + * @param y y coordinate of gate. + * @param r radius of circle. + * + * @returns SVG representation of $\oplus$ symbol. + */ +const _oplus = (x: number, y: number, r = 15): SVGElement => { + const circleBorder: SVGElement = circle(x, y, r); + const vertLine: SVGElement = line(x, y - r, x, y + r); + const horLine: SVGElement = line(x - r, y, x + r, y); + return group([circleBorder, vertLine, horLine], { class: "oplus" }); +}; + +/** + * Generates the SVG for a group of nested operations. + * + * @param metadata Metadata representation of gate. + * @param nestedDepth Depth of nested operations (used in classically controlled and grouped operations). + * + * @returns SVG representation of gate. + */ +const _groupedOperations = ( + metadata: Metadata, + nestedDepth: number, +): SVGElement => { + const { children } = metadata; + const [x1, y1, x2, y2] = _gatePosition(metadata, nestedDepth); + + // Draw dashed box around children gates + const box: SVGElement = dashedBox(x1, y1, x2, y2); + const elems: SVGElement[] = [box]; + if (children != null) + elems.push(formatGates(children as Metadata[], nestedDepth + 1)); + return _createGate(elems, metadata, nestedDepth); +}; + +/** + * Generates the SVG for a classically controlled group of operations. + * + * @param metadata Metadata representation of gate. + * @param padding Padding within dashed box. + * + * @returns SVG representation of gate. + */ +const _classicalControlled = ( + metadata: Metadata, + padding: number = groupBoxPadding, +): SVGElement => { + const { controlsY, dataAttributes } = metadata; + const targetsY: number[] = metadata.targetsY as number[]; + const children: Metadata[][] = metadata.children as Metadata[][]; + let { x, width } = metadata; + + const controlY = controlsY[0]; + + const elems: SVGElement[] = []; + + if (children != null) { + if (children.length !== 2) + throw new Error( + `Invalid number of children found for classically-controlled gate: ${children.length}`, + ); + + // Get SVG for gates controlled on 0 + const childrenZero: SVGElement = formatGates(children[0]); + childrenZero.setAttribute("class", "gates-zero"); + elems.push(childrenZero); + + // Get SVG for gates controlled on 1 + const childrenOne: SVGElement = formatGates(children[1]); + childrenOne.setAttribute("class", "gates-one"); + elems.push(childrenOne); + } + + // Draw control button and attached dashed line to dashed box + const controlCircleX: number = x + controlBtnRadius; + const controlCircle: SVGElement = _controlCircle(controlCircleX, controlY); + const lineY1: number = controlY + controlBtnRadius, + lineY2: number = controlY + classicalRegHeight / 2; + const vertLine: SVGElement = dashedLine( + controlCircleX, + lineY1, + controlCircleX, + lineY2, + "classical-line", + ); + x += controlBtnOffset; + const horLine: SVGElement = dashedLine( + controlCircleX, + lineY2, + x, + lineY2, + "classical-line", + ); + + width = width - controlBtnOffset + (padding - groupBoxPadding) * 2; + x += groupBoxPadding - padding; + const y: number = targetsY[0] - gateHeight / 2 - padding; + const height: number = targetsY[1] - targetsY[0] + gateHeight + padding * 2; + + // Draw dashed box around children gates + const box: SVGElement = dashedBox(x, y, width, height, "classical-container"); + + elems.push(...[horLine, vertLine, controlCircle, box]); + + // Display controlled operation in initial "unknown" state + const attributes: { [attr: string]: string } = { + class: `classically-controlled-group classically-controlled-unknown`, + }; + if (dataAttributes != null) + Object.entries(dataAttributes).forEach( + ([attr, val]) => (attributes[`data-${attr}`] = val), + ); + + return group(elems, attributes); +}; + +/** + * Generates the SVG representation of the control circle on a classical register with interactivity support + * for toggling between bit values (unknown, 1, and 0). + * + * @param x x coord. + * @param y y coord. + * @param r Radius of circle. + * + * @returns SVG representation of control circle. + */ +const _controlCircle = ( + x: number, + y: number, + r: number = controlBtnRadius, +): SVGElement => + group([circle(x, y, r), text("?", x, y, labelFontSize)], { + class: "classically-controlled-btn", + }); + +export { + formatGates, + _formatGate, + _createGate, + _zoomButton, + _measure, + _unitary, + _swap, + _controlledGate, + _groupedOperations, + _classicalControlled, +}; diff --git a/circuit_vis/src/formatters/inputFormatter.ts b/circuit_vis/src/formatters/inputFormatter.ts new file mode 100644 index 0000000000..7464de4c33 --- /dev/null +++ b/circuit_vis/src/formatters/inputFormatter.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Qubit } from "../circuit"; +import { RegisterType, RegisterMap, RegisterMetadata } from "../register"; +import { + leftPadding, + startY, + registerHeight, + classicalRegHeight, +} from "../constants"; +import { group, text } from "./formatUtils"; + +/** + * `formatInputs` takes in an array of Qubits and outputs the SVG string of formatted + * qubit wires and a mapping from register IDs to register metadata (for rendering). + * + * @param qubits List of declared qubits. + * + * @returns returns the SVG string of formatted qubit wires, a mapping from registers + * to y coord and total SVG height. + */ +const formatInputs = ( + qubits: Qubit[], +): { qubitWires: SVGElement; registers: RegisterMap; svgHeight: number } => { + const qubitWires: SVGElement[] = []; + const registers: RegisterMap = {}; + + let currY: number = startY; + qubits.forEach(({ id, numChildren }) => { + // Add qubit wire to list of qubit wires + qubitWires.push(_qubitInput(currY)); + + // Create qubit register + registers[id] = { type: RegisterType.Qubit, y: currY }; + + // If there are no attached classical registers, increment y by fixed register height + if (numChildren == null || numChildren === 0) { + currY += registerHeight; + return; + } + + // Increment current height by classical register height for attached classical registers + currY += classicalRegHeight; + + // Add classical wires + registers[id].children = Array.from(Array(numChildren), () => { + const clsReg: RegisterMetadata = { + type: RegisterType.Classical, + y: currY, + }; + currY += classicalRegHeight; + return clsReg; + }); + }); + + return { + qubitWires: group(qubitWires), + registers, + svgHeight: currY, + }; +}; + +/** + * Generate the SVG text component for the input qubit register. + * + * @param y y coord of input wire to render in SVG. + * + * @returns SVG text component for the input register. + */ +const _qubitInput = (y: number): SVGElement => { + const el: SVGElement = text("|0⟩", leftPadding, y, 16); + el.setAttribute("text-anchor", "start"); + el.setAttribute("dominant-baseline", "middle"); + return el; +}; + +export { formatInputs, _qubitInput }; diff --git a/circuit_vis/src/formatters/registerFormatter.ts b/circuit_vis/src/formatters/registerFormatter.ts new file mode 100644 index 0000000000..aa858e9278 --- /dev/null +++ b/circuit_vis/src/formatters/registerFormatter.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { RegisterMap } from "../register"; +import { regLineStart } from "../constants"; +import { Metadata, GateType } from "../metadata"; +import { group, line, text } from "./formatUtils"; + +/** + * Generate the SVG representation of the qubit register wires in `registers` and the classical wires + * stemming from each measurement gate. + * + * @param registers Map from register IDs to register metadata. + * @param measureGates Array of measurement gates metadata. + * @param endX End x coord. + * + * @returns SVG representation of register wires. + */ +const formatRegisters = ( + registers: RegisterMap, + measureGates: Metadata[], + endX: number, +): SVGElement => { + const formattedRegs: SVGElement[] = []; + // Render qubit wires + for (const qId in registers) { + formattedRegs.push(_qubitRegister(Number(qId), endX, registers[qId].y)); + } + // Render classical wires + measureGates.forEach(({ type, x, targetsY, controlsY }) => { + if (type !== GateType.Measure) return; + const gateY: number = controlsY[0]; + (targetsY as number[]).forEach((y) => { + formattedRegs.push(_classicalRegister(x, gateY, endX, y)); + }); + }); + return group(formattedRegs); +}; + +/** + * Generates the SVG representation of a classical register. + * + * @param startX Start x coord. + * @param gateY y coord of measurement gate. + * @param endX End x coord. + * @param wireY y coord of wire. + * + * @returns SVG representation of the given classical register. + */ +const _classicalRegister = ( + startX: number, + gateY: number, + endX: number, + wireY: number, +): SVGElement => { + const wirePadding = 1; + // Draw vertical lines + const vLine1: SVGElement = line( + startX + wirePadding, + gateY, + startX + wirePadding, + wireY - wirePadding, + "register-classical", + ); + const vLine2: SVGElement = line( + startX - wirePadding, + gateY, + startX - wirePadding, + wireY + wirePadding, + "register-classical", + ); + + // Draw horizontal lines + const hLine1: SVGElement = line( + startX + wirePadding, + wireY - wirePadding, + endX, + wireY - wirePadding, + "register-classical", + ); + const hLine2: SVGElement = line( + startX - wirePadding, + wireY + wirePadding, + endX, + wireY + wirePadding, + "register-classical", + ); + + return group([vLine1, vLine2, hLine1, hLine2]); +}; + +/** + * Generates the SVG representation of a qubit register. + * + * @param qId Qubit register index. + * @param endX End x coord. + * @param y y coord of wire. + * @param labelOffset y offset for wire label. + * + * @returns SVG representation of the given qubit register. + */ +const _qubitRegister = ( + qId: number, + endX: number, + y: number, + labelOffset = 16, +): SVGElement => { + const wire: SVGElement = line(regLineStart, y, endX, y); + + const label: SVGElement = text(`q${qId}`, regLineStart, y - labelOffset); + label.setAttribute("dominant-baseline", "hanging"); + label.setAttribute("text-anchor", "start"); + label.setAttribute("font-size", "75%"); + + return group([wire, label]); +}; + +export { formatRegisters, _classicalRegister, _qubitRegister }; diff --git a/circuit_vis/src/index.ts b/circuit_vis/src/index.ts new file mode 100644 index 0000000000..5e37c199f8 --- /dev/null +++ b/circuit_vis/src/index.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Sqore } from "./sqore"; +import { Circuit } from "./circuit"; +import { StyleConfig } from "./styles"; + +/** + * Render `circuit` into `container` at the specified layer depth. + * + * @param circuit Circuit to be visualized. + * @param container HTML element for rendering visualization into. + * @param style Custom visualization style. + * @param renderDepth Initial layer depth at which to render gates. + */ +export const draw = ( + circuit: Circuit, + container: HTMLElement, + style: StyleConfig | string = {}, + renderDepth = 0, +): void => { + const sqore = new Sqore(circuit, style); + sqore.draw(container, renderDepth); +}; + +export { STYLES } from "./styles"; + +// Export types +export type { StyleConfig } from "./styles"; +export type { Circuit, Qubit, Operation } from "./circuit"; diff --git a/circuit_vis/src/metadata.ts b/circuit_vis/src/metadata.ts new file mode 100644 index 0000000000..4c5c265289 --- /dev/null +++ b/circuit_vis/src/metadata.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { DataAttributes } from "./circuit"; + +/** + * Enum for the various gate operations handled. + */ +export enum GateType { + /** Measurement gate. */ + Measure, + /** CNOT gate. */ + Cnot, + /** SWAP gate. */ + Swap, + /** X gate. */ + X, + /** Single/multi qubit unitary gate. */ + Unitary, + /** Single/multi controlled unitary gate. */ + ControlledUnitary, + /** Nested group of classically-controlled gates. */ + ClassicalControlled, + /** Group of nested gates */ + Group, + /** Invalid gate. */ + Invalid, +} + +/** + * Metadata used to store information pertaining to a given + * operation for rendering its corresponding SVG. + */ +export interface Metadata { + /** Gate type. */ + type: GateType; + /** Centre x coord for gate position. */ + x: number; + /** Array of y coords of control registers. */ + controlsY: number[]; + /** Array of y coords of target registers. + * For `GateType.Unitary` or `GateType.ControlledUnitary`, this is an array of groups of + * y coords, where each group represents a unitary box to be rendered separately. + */ + targetsY: (number | number[])[]; + /** Gate label. */ + label: string; + /** Gate arguments as string. */ + displayArgs?: string; + /** Gate width. */ + width: number; + /** Children operations as part of group. */ + children?: (Metadata | Metadata[])[]; + /** Custom data attributes to attach to gate element. */ + dataAttributes?: DataAttributes; +} diff --git a/circuit_vis/src/process.ts b/circuit_vis/src/process.ts new file mode 100644 index 0000000000..f18d7be1cf --- /dev/null +++ b/circuit_vis/src/process.ts @@ -0,0 +1,536 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + minGateWidth, + startX, + gatePadding, + controlBtnOffset, + groupBoxPadding, +} from "./constants"; +import { Operation, ConditionalRender } from "./circuit"; +import { Metadata, GateType } from "./metadata"; +import { Register, RegisterMap, RegisterType } from "./register"; +import { getGateWidth } from "./utils"; + +/** + * Takes in a list of operations and maps them to `metadata` objects which + * contains information for formatting the corresponding SVG. + * + * @param operations Array of operations. + * @param registers Mapping from qubit IDs to register metadata. + * + * @returns An object containing `metadataList` (Array of Metadata objects) and + * `svgWidth` which is the width of the entire SVG. + */ +const processOperations = ( + operations: Operation[], + registers: RegisterMap, +): { metadataList: Metadata[]; svgWidth: number } => { + if (operations.length === 0) return { metadataList: [], svgWidth: startX }; + + // Group operations based on registers + const groupedOps: number[][] = _groupOperations(operations, registers); + + // Align operations on multiple registers + const alignedOps: (number | null)[][] = _alignOps(groupedOps); + + // Maintain widths of each column to account for variable-sized gates + const numColumns: number = Math.max( + 0, + ...alignedOps.map((ops) => ops.length), + ); + const columnsWidths: number[] = new Array(numColumns).fill(minGateWidth); + + // Get classical registers and their starting column index + const classicalRegs: [number, Register][] = _getClassicalRegStart( + operations, + alignedOps, + ); + + // Keep track of which ops are already seen to avoid duplicate rendering + const visited: { [opIdx: number]: boolean } = {}; + + // Map operation index to gate metadata for formatting later + const opsMetadata: Metadata[][] = alignedOps.map((regOps) => + regOps.map((opIdx, col) => { + let op: Operation | null = null; + + // eslint-disable-next-line no-prototype-builtins + if (opIdx != null && !visited.hasOwnProperty(opIdx)) { + op = operations[opIdx]; + visited[opIdx] = true; + } + + const metadata: Metadata = _opToMetadata(op, registers); + + if ( + op != null && + [GateType.Unitary, GateType.ControlledUnitary].includes(metadata.type) + ) { + // If gate is a unitary type, split targetsY into groups if there + // is a classical register between them for rendering + + // Get y coordinates of classical registers in the same column as this operation + const classicalRegY: number[] = classicalRegs + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([regCol, _]) => regCol <= col) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(([_, reg]) => { + if (reg.cId == null) + throw new Error("Could not find cId for classical register."); + const { children } = registers[reg.qId]; + if (children == null) + throw new Error( + `Failed to find classical registers for qubit ID ${reg.qId}.`, + ); + return children[reg.cId].y; + }); + + metadata.targetsY = _splitTargetsY( + op.targets, + classicalRegY, + registers, + ); + } + + // Expand column size, if needed + if (metadata.width > columnsWidths[col]) { + columnsWidths[col] = metadata.width; + } + + return metadata; + }), + ); + + // Fill in x coord of each gate + const endX: number = _fillMetadataX(opsMetadata, columnsWidths); + + // Flatten operations and filter out invalid gates + const metadataList: Metadata[] = opsMetadata + .flat() + .filter(({ type }) => type != GateType.Invalid); + + return { metadataList, svgWidth: endX }; +}; + +/** + * Group gates provided by operations into their respective registers. + * + * @param operations Array of operations. + * @param numRegs Total number of registers. + * + * @returns 2D array of indices where `groupedOps[i][j]` is the index of the operations + * at register `i` and column `j` (not yet aligned/padded). + */ +const _groupOperations = ( + operations: Operation[], + registers: RegisterMap, +): number[][] => { + // NOTE: We get the max ID instead of just number of keys because there can be a qubit ID that + // isn't acted upon and thus does not show up as a key in registers. + const numRegs: number = + Math.max(-1, ...Object.keys(registers).map(Number)) + 1; + const groupedOps: number[][] = Array.from(Array(numRegs), () => new Array(0)); + operations.forEach(({ targets, controls }, instrIdx) => { + const ctrls: Register[] = controls || []; + const qRegs: Register[] = [...ctrls, ...targets].filter( + ({ type }) => (type || RegisterType.Qubit) === RegisterType.Qubit, + ); + const qRegIdxList: number[] = qRegs.map(({ qId }) => qId); + const clsControls: Register[] = ctrls.filter( + ({ type }) => (type || RegisterType.Qubit) === RegisterType.Classical, + ); + const isClassicallyControlled: boolean = clsControls.length > 0; + if (!isClassicallyControlled && qRegs.length === 0) return; + // If operation is classically-controlled, pad all qubit registers. Otherwise, only pad + // the contiguous range of registers that it covers. + const minRegIdx: number = isClassicallyControlled + ? 0 + : Math.min(...qRegIdxList); + const maxRegIdx: number = isClassicallyControlled + ? numRegs - 1 + : Math.max(...qRegIdxList); + // Add operation also to registers that are in-between target registers + // so that other gates won't render in the middle. + for (let i = minRegIdx; i <= maxRegIdx; i++) { + groupedOps[i].push(instrIdx); + } + }); + return groupedOps; +}; + +/** + * Aligns operations by padding registers with `null`s to make sure that multiqubit + * gates are in the same column. + * e.g. ---[x]---[x]-- + * ----------|--- + * + * @param ops 2D array of operations. Each row represents a register + * and the operations acting on it (in-order). + * + * @returns 2D array of aligned operations padded with `null`s. + */ +const _alignOps = (ops: number[][]): (number | null)[][] => { + let maxNumOps: number = Math.max(0, ...ops.map((regOps) => regOps.length)); + let col = 0; + // Deep copy ops to be returned as paddedOps + const paddedOps: (number | null)[][] = JSON.parse(JSON.stringify(ops)); + while (col < maxNumOps) { + for (let regIdx = 0; regIdx < paddedOps.length; regIdx++) { + const reg: (number | null)[] = paddedOps[regIdx]; + if (reg.length <= col) continue; + + // Should never be null (nulls are only padded to previous columns) + const opIdx: number | null = reg[col]; + + // Get position of gate + const targetsPos: number[] = paddedOps.map((regOps) => + regOps.indexOf(opIdx), + ); + const gatePos: number = Math.max(-1, ...targetsPos); + + // If current column is not desired gate position, pad with null + if (col < gatePos) { + paddedOps[regIdx].splice(col, 0, null); + maxNumOps = Math.max(maxNumOps, paddedOps[regIdx].length); + } + } + col++; + } + return paddedOps; +}; + +/** + * Retrieves the starting index of each classical register. + * + * @param ops Array of operations. + * @param idxList 2D array of aligned operation indices. + * + * @returns Array of classical register and their starting column indices in the form [[column, register]]. + */ +const _getClassicalRegStart = ( + ops: Operation[], + idxList: (number | null)[][], +): [number, Register][] => { + const clsRegs: [number, Register][] = []; + idxList.forEach((reg) => { + for (let col = 0; col < reg.length; col++) { + const opIdx: number | null = reg[col]; + if (opIdx != null && ops[opIdx].isMeasurement) { + const targetClsRegs: Register[] = ops[opIdx].targets.filter( + (reg) => reg.type === RegisterType.Classical, + ); + targetClsRegs.forEach((reg) => clsRegs.push([col, reg])); + } + } + }); + return clsRegs; +}; + +/** + * Maps operation to metadata (e.g. gate type, position, dimensions, text) + * required to render the image. + * + * @param op Operation to be mapped into metadata format. + * @param registers Array of registers. + * + * @returns Metadata representation of given operation. + */ +const _opToMetadata = ( + op: Operation | null, + registers: RegisterMap, +): Metadata => { + const metadata: Metadata = { + type: GateType.Invalid, + x: 0, + controlsY: [], + targetsY: [], + label: "", + width: -1, + }; + + if (op == null) return metadata; + + const { + gate, + dataAttributes, + displayArgs, + isMeasurement, + isConditional, + isControlled, + isAdjoint, + controls, + targets, + children, + conditionalRender, + } = op; + + // Set y coords + metadata.controlsY = controls?.map((reg) => _getRegY(reg, registers)) || []; + metadata.targetsY = targets.map((reg) => _getRegY(reg, registers)); + + if (isConditional) { + // Classically-controlled operations + if (children == null || children.length == 0) + throw new Error( + "No children operations found for classically-controlled operation.", + ); + + // Gates to display when classical bit is 0. + const onZeroOps: Operation[] = children.filter( + (op) => op.conditionalRender !== ConditionalRender.OnOne, + ); + let childrenInstrs = processOperations(onZeroOps, registers); + const zeroGates: Metadata[] = childrenInstrs.metadataList; + const zeroChildWidth: number = childrenInstrs.svgWidth; + + // Gates to display when classical bit is 1. + const onOneOps: Operation[] = children.filter( + (op) => op.conditionalRender !== ConditionalRender.OnZero, + ); + childrenInstrs = processOperations(onOneOps, registers); + const oneGates: Metadata[] = childrenInstrs.metadataList; + const oneChildWidth: number = childrenInstrs.svgWidth; + + // Subtract startX (left-side) and 2*gatePadding (right-side) from nested child gates width + const width: number = + Math.max(zeroChildWidth, oneChildWidth) - startX - gatePadding * 2; + + metadata.type = GateType.ClassicalControlled; + metadata.children = [zeroGates, oneGates]; + // Add additional width from control button and inner box padding for dashed box + metadata.width = width + controlBtnOffset + groupBoxPadding * 2; + + // Set targets to first and last quantum registers so we can render the surrounding box + // around all quantum registers. + const qubitsY: number[] = Object.values(registers).map(({ y }) => y); + if (qubitsY.length > 0) + metadata.targetsY = [Math.min(...qubitsY), Math.max(...qubitsY)]; + } else if ( + conditionalRender == ConditionalRender.AsGroup && + (children?.length || 0) > 0 + ) { + const childrenInstrs = processOperations( + children as Operation[], + registers, + ); + metadata.type = GateType.Group; + metadata.children = childrenInstrs.metadataList; + // _zoomButton function in gateFormatter.ts relies on + // 'expanded' attribute to render zoom button + metadata.dataAttributes = { expanded: "true" }; + // Subtract startX (left-side) and add inner box padding (minus nested gate padding) for dashed box + metadata.width = + childrenInstrs.svgWidth - startX + (groupBoxPadding - gatePadding) * 2; + } else if (isMeasurement) { + metadata.type = GateType.Measure; + } else if (gate === "SWAP") { + metadata.type = GateType.Swap; + } else if (isControlled) { + metadata.type = gate === "X" ? GateType.Cnot : GateType.ControlledUnitary; + metadata.label = gate; + } else if (gate === "X") { + metadata.type = GateType.X; + metadata.label = gate; + } else { + // Any other gate treated as a simple unitary gate + metadata.type = GateType.Unitary; + metadata.label = gate; + } + + // If adjoint, add ' to the end of gate label + if (isAdjoint && metadata.label.length > 0) metadata.label += "'"; + + // If gate has extra arguments, display them + if (displayArgs != null) metadata.displayArgs = displayArgs; + + // Set gate width + metadata.width = getGateWidth(metadata); + + // Extend existing data attributes with user-provided data attributes + if (dataAttributes != null) + metadata.dataAttributes = { ...metadata.dataAttributes, ...dataAttributes }; + + return metadata; +}; + +/** + * Compute the y coord of a given register. + * + * @param reg Register to compute y coord of. + * @param registers Map of qubit IDs to RegisterMetadata. + * + * @returns The y coord of give register. + */ +const _getRegY = (reg: Register, registers: RegisterMap): number => { + const { type, qId, cId } = reg; + if (!Object.prototype.hasOwnProperty.call(registers, qId)) + throw new Error(`ERROR: Qubit register with ID ${qId} not found.`); + const { y, children } = registers[qId]; + switch (type) { + case undefined: + case RegisterType.Qubit: + return y; + case RegisterType.Classical: + if (children == null) + throw new Error( + `ERROR: No classical registers found for qubit ID ${qId}.`, + ); + if (cId == null) + throw new Error( + `ERROR: No ID defined for classical register associated with qubit ID ${qId}.`, + ); + if (children.length <= cId) + throw new Error( + `ERROR: Classical register ID ${cId} invalid for qubit ID ${qId} with ${children.length} classical register(s).`, + ); + return children[cId].y; + default: + throw new Error(`ERROR: Unknown register type ${type}.`); + } +}; + +/** + * Splits `targets` if non-adjacent or intersected by classical registers. + * + * @param targets Target qubit registers. + * @param classicalRegY y coords of classical registers overlapping current column. + * @param registers Mapping from register qubit IDs to register metadata. + * + * @returns Groups of target qubit y coords. + */ +const _splitTargetsY = ( + targets: Register[], + classicalRegY: number[], + registers: RegisterMap, +): number[][] => { + if (targets.length === 0) return []; + + // Get qIds sorted by ascending y value + const orderedQIds: number[] = Object.keys(registers).map(Number); + orderedQIds.sort((a, b) => registers[a].y - registers[b].y); + const qIdPosition: { [qId: number]: number } = {}; + orderedQIds.forEach((qId, i) => (qIdPosition[qId] = i)); + + // Sort targets and classicalRegY by ascending y value + targets = targets.slice(); + targets.sort((a, b) => { + const posDiff: number = qIdPosition[a.qId] - qIdPosition[b.qId]; + if (posDiff === 0 && a.cId != null && b.cId != null) return a.cId - b.cId; + else return posDiff; + }); + classicalRegY = classicalRegY.slice(); + classicalRegY.sort((a, b) => a - b); + + let prevPos = 0; + let prevY = 0; + + return targets.reduce((groups: number[][], target: Register) => { + const y = _getRegY(target, registers); + const pos = qIdPosition[target.qId]; + + // Split into new group if one of the following holds: + // 1. First target register + // 2. Non-adjacent qubit registers + // 3. There is a classical register between current and previous register + if ( + groups.length === 0 || + pos > prevPos + 1 || + (classicalRegY[0] > prevY && classicalRegY[0] < y) + ) + groups.push([y]); + else groups[groups.length - 1].push(y); + + prevPos = pos; + prevY = y; + + // Remove classical registers that are higher than current y + while (classicalRegY.length > 0 && classicalRegY[0] <= y) + classicalRegY.shift(); + + return groups; + }, []); +}; + +/** + * Updates the x coord of each metadata in the given 2D array of metadata and returns rightmost x coord. + * + * @param opsMetadata 2D array of metadata. + * @param columnWidths Array of column widths. + * + * @returns Rightmost x coord. + */ +const _fillMetadataX = ( + opsMetadata: Metadata[][], + columnWidths: number[], +): number => { + let currX: number = startX; + + const colStartX: number[] = columnWidths.map((width) => { + const x: number = currX; + currX += width + gatePadding * 2; + return x; + }); + + const endX: number = currX; + + opsMetadata.forEach((regOps) => + regOps.forEach((metadata, col) => { + const x = colStartX[col]; + switch (metadata.type) { + case GateType.ClassicalControlled: + case GateType.Group: + { + // Subtract startX offset from nested gates and add offset and padding + let offset: number = x - startX + groupBoxPadding; + if (metadata.type === GateType.ClassicalControlled) + offset += controlBtnOffset; + + // Offset each x coord in children gates + _offsetChildrenX(metadata.children, offset); + + // We don't use the centre x coord because we only care about the rightmost x for + // rendering the box around the group of nested gates + metadata.x = x; + } + break; + + default: + metadata.x = x + columnWidths[col] / 2; + break; + } + }), + ); + + return endX; +}; + +/** + * Offset x coords of nested children operations. + * + * @param children 2D array of children metadata. + * @param offset x coord offset. + */ +const _offsetChildrenX = ( + children: (Metadata | Metadata[])[] | undefined, + offset: number, +): void => { + if (children == null) return; + children.flat().forEach((child) => { + child.x += offset; + _offsetChildrenX(child.children, offset); + }); +}; + +export { + processOperations, + _groupOperations, + _alignOps, + _getClassicalRegStart, + _opToMetadata, + _getRegY, + _splitTargetsY, + _fillMetadataX, + _offsetChildrenX, +}; diff --git a/circuit_vis/src/register.ts b/circuit_vis/src/register.ts new file mode 100644 index 0000000000..e2fb0588fa --- /dev/null +++ b/circuit_vis/src/register.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Type of register. + */ +export enum RegisterType { + Qubit, + Classical, +} + +/** + * Represents a register resource. + */ +export interface Register { + /** Type of register. If missing defaults to Qubit. */ + type?: RegisterType; + /** Qubit register ID. */ + qId: number; + /** Classical register ID (if classical register). */ + cId?: number; +} + +/** + * Metadata for qubit register. + */ +export interface RegisterMetadata { + /** Type of register. */ + type: RegisterType; + /** y coord of register */ + y: number; + /** Nested classical registers attached to quantum register. */ + children?: RegisterMetadata[]; +} + +/** + * Mapping from qubit IDs to their register metadata. + */ +export interface RegisterMap { + [id: number]: RegisterMetadata; +} diff --git a/circuit_vis/src/sqore.ts b/circuit_vis/src/sqore.ts new file mode 100644 index 0000000000..2c3263265f --- /dev/null +++ b/circuit_vis/src/sqore.ts @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { formatInputs } from "./formatters/inputFormatter"; +import { formatGates } from "./formatters/gateFormatter"; +import { formatRegisters } from "./formatters/registerFormatter"; +import { processOperations } from "./process"; +import { ConditionalRender, Circuit, Operation } from "./circuit"; +import { Metadata, GateType } from "./metadata"; +import { StyleConfig, style, STYLES } from "./styles"; +import { createUUID } from "./utils"; +import { svgNS } from "./constants"; + +/** + * Contains metadata for visualization. + */ +interface ComposedSqore { + /** Width of visualization. */ + width: number; + /** Height of visualization. */ + height: number; + /** SVG elements the make up the visualization. */ + elements: SVGElement[]; +} + +/** + * Defines the mapping of unique ID to each operation. Used for enabling + * interactivity. + */ +type GateRegistry = { + [id: string]: Operation; +}; + +/** + * Entrypoint class for rendering circuit visualizations. + */ +export class Sqore { + circuit: Circuit; + style: StyleConfig = {}; + gateRegistry: GateRegistry = {}; + + /** + * Initializes Sqore object with custom styles. + * + * @param circuit Circuit to be visualized. + * @param style Custom visualization style. + */ + constructor(circuit: Circuit, style: StyleConfig | string = {}) { + this.circuit = circuit; + this.style = this.getStyle(style); + } + + /** + * Render circuit into `container` at the specified layer depth. + * + * @param container HTML element for rendering visualization into. + * @param renderDepth Initial layer depth at which to render gates. + */ + draw(container: HTMLElement, renderDepth = 0): void { + // Inject into container + if (container == null) throw new Error(`Container not provided.`); + + // Create copy of circuit to prevent mutation + const circuit: Circuit = JSON.parse(JSON.stringify(this.circuit)); + + // Assign unique IDs to each operation + circuit.operations.forEach((op, i) => + this.fillGateRegistry(op, i.toString()), + ); + + // Render operations at starting at given depth + circuit.operations = this.selectOpsAtDepth(circuit.operations, renderDepth); + + // If only one top-level operation, expand automatically: + if ( + circuit.operations.length == 1 && + circuit.operations[0].dataAttributes != null && + // eslint-disable-next-line no-prototype-builtins + circuit.operations[0].dataAttributes.hasOwnProperty("id") + ) { + const id: string = circuit.operations[0].dataAttributes["id"]; + this.expandOperation(circuit.operations, id); + } + + this.renderCircuit(container, circuit); + } + + /** + * Retrieve style for visualization. + * + * @param style Custom style or style name. + * + * @returns Custom style. + */ + private getStyle(style: StyleConfig | string = {}): StyleConfig { + if (typeof style === "string" || style instanceof String) { + const styleName: string = style as string; + // eslint-disable-next-line no-prototype-builtins + if (!STYLES.hasOwnProperty(styleName)) { + console.error(`No style ${styleName} found in STYLES.`); + return {}; + } + style = STYLES[styleName]; + } + return style; + } + + /** + * Render circuit into `container`. + * + * @param container HTML element for rendering visualization into. + * @param circuit Circuit object to be rendered. + */ + private renderCircuit(container: HTMLElement, circuit: Circuit): void { + // Create visualization components + const composedSqore: ComposedSqore = this.compose(circuit); + const svg: SVGElement = this.generateSvg(composedSqore); + container.innerHTML = ""; + container.appendChild(svg); + this.addGateClickHandlers(container, circuit); + } + + /** + * Generates the components required for visualization. + * + * @param circuit Circuit to be visualized. + * + * @returns `ComposedSqore` object containing metadata for visualization. + */ + private compose(circuit: Circuit): ComposedSqore { + const add = (acc: Metadata[], gate: Metadata | Metadata[]): void => { + if (Array.isArray(gate)) { + gate.forEach((g) => add(acc, g)); + } else { + acc.push(gate); + gate.children?.forEach((g) => add(acc, g)); + } + }; + + const flatten = (gates: Metadata[]): Metadata[] => { + const result: Metadata[] = []; + add(result, gates); + return result; + }; + + const { qubits, operations } = circuit; + const { qubitWires, registers, svgHeight } = formatInputs(qubits); + const { metadataList, svgWidth } = processOperations(operations, registers); + const formattedGates: SVGElement = formatGates(metadataList); + const measureGates: Metadata[] = flatten(metadataList).filter( + ({ type }) => type === GateType.Measure, + ); + const formattedRegs: SVGElement = formatRegisters( + registers, + measureGates, + svgWidth, + ); + + const composedSqore: ComposedSqore = { + width: svgWidth, + height: svgHeight, + elements: [qubitWires, formattedRegs, formattedGates], + }; + return composedSqore; + } + + /** + * Generates visualization of `composedSqore` as an SVG. + * + * @param composedSqore ComposedSqore to be visualized. + * + * @returns SVG representation of circuit visualization. + */ + private generateSvg(composedSqore: ComposedSqore): SVGElement { + const { width, height, elements } = composedSqore; + const uuid: string = createUUID(); + + const svg: SVGElement = document.createElementNS(svgNS, "svg"); + svg.setAttribute("id", uuid); + svg.setAttribute("class", "qviz"); + svg.setAttribute("width", width.toString()); + svg.setAttribute("height", height.toString()); + svg.style.setProperty("max-width", "fit-content"); + + // Add styles + const css = document.createElement("style"); + css.innerHTML = style(this.style); + svg.appendChild(css); + + // Add body elements + elements.forEach((element: SVGElement) => svg.appendChild(element)); + + return svg; + } + + /** + * Depth-first traversal to assign unique ID to `operation`. + * The operation is assigned the id `id` and its `i`th child is recursively given + * the id `${id}-${i}`. + * + * @param operation Operation to be assigned. + * @param id: ID to assign to `operation`. + * + */ + private fillGateRegistry(operation: Operation, id: string): void { + if (operation.dataAttributes == null) operation.dataAttributes = {}; + operation.dataAttributes["id"] = id; + // By default, operations cannot be zoomed-out + operation.dataAttributes["zoom-out"] = "false"; + this.gateRegistry[id] = operation; + operation.children?.forEach((childOp, i) => { + this.fillGateRegistry(childOp, `${id}-${i}`); + if (childOp.dataAttributes == null) childOp.dataAttributes = {}; + // Children operations can be zoomed out + childOp.dataAttributes["zoom-out"] = "true"; + }); + // Composite operations can be zoomed in + operation.dataAttributes["zoom-in"] = ( + operation.children != null + ).toString(); + } + + /** + * Pick out operations that are at or below `renderDepth`. + * + * @param operations List of circuit operations. + * @param renderDepth Initial layer depth at which to render gates. + * + * @returns List of operations at or below specifed depth. + */ + private selectOpsAtDepth( + operations: Operation[], + renderDepth: number, + ): Operation[] { + if (renderDepth < 0) + throw new Error( + `Invalid renderDepth of ${renderDepth}. Needs to be >= 0.`, + ); + if (renderDepth === 0) return operations; + return operations + .map((op) => + op.children != null + ? this.selectOpsAtDepth(op.children, renderDepth - 1) + : op, + ) + .flat(); + } + + /** + * Add interactive click handlers to circuit HTML elements. + * + * @param container HTML element containing visualized circuit. + * @param circuit Circuit to be visualized. + * + */ + private addGateClickHandlers(container: HTMLElement, circuit: Circuit): void { + this.addClassicalControlHandlers(container); + this.addZoomHandlers(container, circuit); + } + + /** + * Add interactive click handlers for classically-controlled operations. + * + * @param container HTML element containing visualized circuit. + * + */ + private addClassicalControlHandlers(container: HTMLElement): void { + container.querySelectorAll(".classically-controlled-btn").forEach((btn) => { + // Zoom in on clicked gate + btn.addEventListener("click", (evt: Event) => { + const textSvg = btn.querySelector("text"); + const group = btn.parentElement; + if (textSvg == null || group == null) return; + + const currValue = textSvg.firstChild?.nodeValue; + const zeroGates = group?.querySelector(".gates-zero"); + const oneGates = group?.querySelector(".gates-one"); + switch (currValue) { + case "?": + textSvg.childNodes[0].nodeValue = "1"; + group.classList.remove("classically-controlled-unknown"); + group.classList.remove("classically-controlled-zero"); + group.classList.add("classically-controlled-one"); + zeroGates?.classList.add("hidden"); + oneGates?.classList.remove("hidden"); + break; + case "1": + textSvg.childNodes[0].nodeValue = "0"; + group.classList.remove("classically-controlled-unknown"); + group.classList.add("classically-controlled-zero"); + group.classList.remove("classically-controlled-one"); + zeroGates?.classList.remove("hidden"); + oneGates?.classList.add("hidden"); + break; + case "0": + textSvg.childNodes[0].nodeValue = "?"; + group.classList.add("classically-controlled-unknown"); + group.classList.remove("classically-controlled-zero"); + group.classList.remove("classically-controlled-one"); + zeroGates?.classList.remove("hidden"); + oneGates?.classList.remove("hidden"); + break; + } + evt.stopPropagation(); + }); + }); + } + + /** + * Add interactive click handlers for zoom-in/out functionality. + * + * @param container HTML element containing visualized circuit. + * @param circuit Circuit to be visualized. + * + */ + private addZoomHandlers(container: HTMLElement, circuit: Circuit): void { + container.querySelectorAll(".gate .gate-control").forEach((ctrl) => { + // Zoom in on clicked gate + ctrl.addEventListener("click", (ev: Event) => { + const gateId: string | null | undefined = + ctrl.parentElement?.getAttribute("data-id"); + if (typeof gateId == "string") { + if (ctrl.classList.contains("gate-collapse")) { + this.collapseOperation(circuit.operations, gateId); + } else if (ctrl.classList.contains("gate-expand")) { + this.expandOperation(circuit.operations, gateId); + } + this.renderCircuit(container, circuit); + + ev.stopPropagation(); + } + }); + }); + } + + /** + * Expand selected operation for zoom-in interaction. + * + * @param operations List of circuit operations. + * @param id ID of operation to expand. + * + */ + private expandOperation(operations: Operation[], id: string): void { + operations.forEach((op) => { + if (op.conditionalRender === ConditionalRender.AsGroup) + this.expandOperation(op.children || [], id); + if (op.dataAttributes == null) return op; + const opId: string = op.dataAttributes["id"]; + if (opId === id && op.children != null) { + op.conditionalRender = ConditionalRender.AsGroup; + op.dataAttributes["expanded"] = "true"; + } + }); + } + + /** + * Collapse selected operation for zoom-out interaction. + * + * @param operations List of circuit operations. + * @param id ID of operation to collapse. + * + */ + private collapseOperation(operations: Operation[], parentId: string): void { + operations.forEach((op) => { + if (op.conditionalRender === ConditionalRender.AsGroup) + this.collapseOperation(op.children || [], parentId); + if (op.dataAttributes == null) return op; + const opId: string = op.dataAttributes["id"]; + // Collapse parent gate and its children + if (opId.startsWith(parentId)) { + op.conditionalRender = ConditionalRender.Always; + delete op.dataAttributes["expanded"]; + } + }); + } +} diff --git a/circuit_vis/src/styles.ts b/circuit_vis/src/styles.ts new file mode 100644 index 0000000000..ad22c45b3d --- /dev/null +++ b/circuit_vis/src/styles.ts @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Provides configuration for CSS styles of visualization. + */ +export interface StyleConfig { + /** Line stroke style. */ + lineStroke?: string; + /** Line width. */ + lineWidth?: number; + /** Text colour. */ + textColour?: string; + /** Single qubit unitary fill colour. */ + unitary?: string; + /** Oplus circle fill colour. */ + oplus?: string; + /** Measurement gate fill colour. */ + measure?: string; + /** Measurement unknown primary colour. */ + classicalUnknown?: string; + /** Measurement zero primary colour. */ + classicalZero?: string; + /** Measurement one primary colour. */ + classicalOne?: string; + /** Measurement zero text colour */ + classicalZeroText?: string; + /** Measurement one text colour */ + classicalOneText?: string; +} + +const defaultStyle: StyleConfig = { + lineStroke: "#000000", + lineWidth: 1, + textColour: "#000000", + unitary: "#D9F1FA", + oplus: "#FFFFFF", + measure: "#FFDE86", + classicalUnknown: "#E5E5E5", + classicalZero: "#C40000", + classicalOne: "#4059BD", + classicalZeroText: "#FFFFFF", + classicalOneText: "#FFFFFF", +}; + +const blackAndWhiteStyle: StyleConfig = { + lineStroke: "#000000", + lineWidth: 1, + textColour: "#000000", + unitary: "#FFFFFF", + oplus: "#FFFFFF", + measure: "#FFFFFF", + classicalUnknown: "#FFFFFF", + classicalZero: "#000000", + classicalOne: "#000000", + classicalZeroText: "#FFFFFF", + classicalOneText: "#FFFFFF", +}; + +const invertedStyle: StyleConfig = { + lineStroke: "#FFFFFF", + lineWidth: 1, + textColour: "#FFFFFF", + unitary: "#000000", + oplus: "#000000", + measure: "#000000", + classicalUnknown: "#000000", + classicalZero: "#FFFFFF", + classicalOne: "#FFFFFF", + classicalZeroText: "#000000", + classicalOneText: "#000000", +}; + +/** + * Set of default styles. + */ +export const STYLES: { [name: string]: StyleConfig } = { + /** Default style with coloured gates. */ + Default: defaultStyle, + /** Black and white style. */ + BlackAndWhite: blackAndWhiteStyle, + /** Inverted black and white style (for black backgrounds). */ + Inverted: invertedStyle, +}; + +/** + * CSS style script to be injected into visualization SVG. + * + * @param customStyle Custom style configuration. + * + * @returns String containing CSS style script. + */ +export const style = (customStyle: StyleConfig = {}): string => { + const styleConfig = { ...defaultStyle, ...customStyle }; + + return `${_defaultGates(styleConfig)} + ${_classicallyControlledGates(styleConfig)} + ${_expandCollapse}`; +}; + +const _defaultGates = (styleConfig: StyleConfig): string => ` + line, + circle, + rect { + stroke: ${styleConfig.lineStroke}; + stroke-width: ${styleConfig.lineWidth}; + } + text { + fill: ${styleConfig.textColour}; + dominant-baseline: middle; + text-anchor: middle; + font-family: Arial; + } + .control-dot { + fill: ${styleConfig.lineStroke}; + } + .oplus line, .oplus circle { + fill: ${styleConfig.oplus}; + stroke-width: 2; + } + .gate-unitary { + fill: ${styleConfig.unitary}; + } + .gate-measure { + fill: ${styleConfig.measure}; + } + rect.gate-swap { + fill: transparent; + stroke: transparent; + } + .arc-measure { + stroke: ${styleConfig.lineStroke}; + fill: none; + stroke-width: ${styleConfig.lineWidth}; + } + .register-classical { + stroke-width: ${(styleConfig.lineWidth || 0) / 2}; + }`; + +const _classicallyControlledGates = (styleConfig: StyleConfig): string => { + const gateOutline = ` + .classically-controlled-one .classical-container, + .classically-controlled-one .classical-line { + stroke: ${styleConfig.classicalOne}; + stroke-width: ${(styleConfig.lineWidth || 0) + 0.3}; + fill: ${styleConfig.classicalOne}; + fill-opacity: 0.1; + } + .classically-controlled-zero .classical-container, + .classically-controlled-zero .classical-line { + stroke: ${styleConfig.classicalZero}; + stroke-width: ${(styleConfig.lineWidth || 0) + 0.3}; + fill: ${styleConfig.classicalZero}; + fill-opacity: 0.1; + }`; + const controlBtn = ` + .classically-controlled-btn { + cursor: pointer; + } + .classically-controlled-unknown .classically-controlled-btn { + fill: ${styleConfig.classicalUnknown}; + } + .classically-controlled-one .classically-controlled-btn { + fill: ${styleConfig.classicalOne}; + } + .classically-controlled-zero .classically-controlled-btn { + fill: ${styleConfig.classicalZero}; + }`; + + const controlBtnText = ` + .classically-controlled-btn text { + dominant-baseline: middle; + text-anchor: middle; + stroke: none; + font-family: Arial; + } + .classically-controlled-unknown .classically-controlled-btn text { + fill: ${styleConfig.textColour}; + } + .classically-controlled-one .classically-controlled-btn text { + fill: ${styleConfig.classicalOneText}; + } + .classically-controlled-zero .classically-controlled-btn text { + fill: ${styleConfig.classicalZeroText}; + }`; + + return ` + .hidden { + display: none; + } + .classically-controlled-unknown { + opacity: 0.25; + } + + ${gateOutline} + ${controlBtn} + ${controlBtnText}`; +}; + +const _expandCollapse = ` + .qviz .gate-collapse, + .qviz .gate-expand { + opacity: 0; + transition: opacity 1s; + } + + .qviz:hover .gate-collapse, + .qviz:hover .gate-expand { + visibility: visible; + opacity: 0.2; + transition: visibility 1s; + transition: opacity 1s; + } + + .gate-expand, .gate-collapse { + cursor: pointer; + } + + .gate-collapse circle, + .gate-expand circle { + fill: white; + stroke-width: 2px; + stroke: black; + } + .gate-collapse path, + .gate-expand path { + stroke-width: 4px; + stroke: black; + } + + .gate:hover > .gate-collapse, + .gate:hover > .gate-expand { + visibility: visible; + opacity: 1; + transition: opacity 1s; + }`; diff --git a/circuit_vis/src/utils.ts b/circuit_vis/src/utils.ts new file mode 100644 index 0000000000..8ce9f9d4eb --- /dev/null +++ b/circuit_vis/src/utils.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Metadata, GateType } from "./metadata"; +import { + minGateWidth, + labelPadding, + labelFontSize, + argsFontSize, +} from "./constants"; + +/** + * Generate a UUID using `Math.random`. + * Note: this implementation came from https://stackoverflow.com/questions/105034/how-to-create-guid-uuid + * and is not cryptographically secure but works for our use case. + * + * @returns UUID string. + */ +const createUUID = (): string => + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + +/** + * Calculate the width of a gate, given its metadata. + * + * @param metadata Metadata of a given gate. + * + * @returns Width of given gate (in pixels). + */ +const getGateWidth = ({ + type, + label, + displayArgs, + width, +}: Metadata): number => { + if (width > 0) return width; + + switch (type) { + case GateType.Measure: + case GateType.Cnot: + case GateType.Swap: + return minGateWidth; + default: { + const labelWidth = _getStringWidth(label); + const argsWidth = + displayArgs != null ? _getStringWidth(displayArgs, argsFontSize) : 0; + const textWidth = Math.max(labelWidth, argsWidth) + labelPadding * 2; + return Math.max(minGateWidth, textWidth); + } + } +}; + +/** + * Get the width of a string with font-size `fontSize` and font-family Arial. + * + * @param text Input string. + * @param fontSize Font size of `text`. + * + * @returns Pixel width of given string. + */ +const _getStringWidth = ( + text: string, + fontSize: number = labelFontSize, +): number => { + const canvas: HTMLCanvasElement = document.createElement("canvas"); + const context: CanvasRenderingContext2D | null = canvas.getContext("2d"); + if (context == null) throw new Error("Null canvas"); + + context.font = `${fontSize}px Arial`; + const metrics: TextMetrics = context.measureText(text); + return metrics.width; +}; + +export { createUUID, getGateWidth, _getStringWidth }; diff --git a/circuit_vis/tsconfig.json b/circuit_vis/tsconfig.json new file mode 100644 index 0000000000..85efcd2f95 --- /dev/null +++ b/circuit_vis/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": ["DOM", "ES2019"], + "declaration": true, + "strict": true, + "outDir": "lib", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/circuit_vis/webpack.config.js b/circuit_vis/webpack.config.js new file mode 100644 index 0000000000..fafaa93ab8 --- /dev/null +++ b/circuit_vis/webpack.config.js @@ -0,0 +1,71 @@ +const path = require("path"); +const TerserPlugin = require("terser-webpack-plugin"); + +const PATHS = { + entryPoint: path.resolve(__dirname, "src/index.ts"), + bundles: path.resolve(__dirname, "dist"), +}; + +const config = { + // Enables production mode built-in optimizations + mode: "production", + // These are the entry point of our library. We tell webpack to use + // the name we assign later, when creating the bundle. We also use + // the name to filter the second entry point for applying code + // minification via UglifyJS + entry: { + qviz: [PATHS.entryPoint], + "qviz.min": [PATHS.entryPoint], + }, + // The output defines how and where we want the bundles. The special + // value `[name]` in `filename` tell Webpack to use the name we defined above. + // The bundled script will be available as a global variable `qviz`. + output: { + path: PATHS.bundles, + filename: "[name].js", + library: "qviz", + libraryTarget: "umd", + }, + // Add resolve for `tsx` and `ts` files, otherwise Webpack would + // only look for common JavaScript file extension (.js) + resolve: { + extensions: [".ts", ".tsx", ".js"], + }, + // Activate source maps for the bundles in order to preserve the original + // source when the user debugs the application + devtool: "source-map", + optimization: { + // Apply minification only on the second bundle by + // using a RegEx on the name, which must end with `.min.js` + minimize: true, + minimizer: [ + new TerserPlugin({ + // sourceMap: true, + include: /\.min\.js$/, + }), + ], + }, + module: { + // Webpack doesn't understand TypeScript files and a loader is needed. + // `node_modules` folder is excluded in order to prevent problems with + // the library dependencies, as well as `__tests__` folders that + // contain the tests for the library + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: [ + { + loader: "ts-loader", + options: { + // Speeds up compilation and does not build *.d.ts files + transpileOnly: true, + }, + }, + ], + }, + ], + }, +}; + +module.exports = config; diff --git a/eslint.config.mjs b/eslint.config.mjs index 96c46336f9..e32ba008c8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,10 @@ export default tseslint.config( }, { ignores: [ + "circuit_vis/lib/", + "circuit_vis/coverage/", + "circuit_vis/dist/", + "circuit_vis/*.config.js", "target/", "playground/public/", "npm/qsharp/dist/", diff --git a/package-lock.json b/package-lock.json index e82464764d..75384129aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,12 @@ "workspaces": [ "./npm/qsharp", "./playground", - "./vscode" + "./vscode", + "./circuit_vis" ], "devDependencies": { "@anywidget/types": "^0.1.4", "@eslint/js": "^9.9.1", - "@microsoft/quantum-viz.js": "^1.0.5", "@parcel/watcher": "^2.4.1", "@types/chai": "^4.3.8", "@types/markdown-it": "^14.1.1", @@ -40,16 +40,25 @@ "preact": "^10.20.0", "prettier": "^3.3.3", "punycode": "^2.3.1", + "terser-webpack-plugin": "^5.3.11", + "ts-loader": "^9.5.2", "typescript": "^5.5.4", "typescript-eslint": "^8.19.1", "url": "^0.11.3", "user-agent-data-types": "^0.4.2", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.0.8", + "webpack": "^5.97.1", + "webpack-cli": "^6.0.1" }, "engines": { "node": ">=18.17.1" } }, + "circuit_vis": { + "name": "@microsoft/quantum-viz.js", + "version": "1.0.6", + "license": "MIT" + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -232,6 +241,15 @@ "node": ">=14.0.0" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "engines": { + "node": ">=14.17.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -954,6 +972,64 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jupyter-widgets/base": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/@jupyter-widgets/base/-/base-6.0.7.tgz", @@ -1452,10 +1528,8 @@ "dev": true }, "node_modules/@microsoft/quantum-viz.js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@microsoft/quantum-viz.js/-/quantum-viz.js-1.0.5.tgz", - "integrity": "sha512-U2atugL4j+p/yiioPR/TfqqR534iP6kEa10S+YNUW68UyvMe8eXNDFlb8GG4Ro9WSGobyLa8tP7iRiR2YMZO1w==", - "dev": true + "resolved": "circuit_vis", + "link": true }, "node_modules/@nevware21/ts-async": { "version": "0.5.0", @@ -1922,6 +1996,26 @@ "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2309,6 +2403,208 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2380,6 +2676,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2646,6 +2971,38 @@ "pako": "~0.2.0" } }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2705,6 +3062,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chai": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", @@ -2790,6 +3167,15 @@ "node": ">= 6" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -2807,6 +3193,20 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/cls-hooked": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", @@ -2858,6 +3258,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3140,6 +3546,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "node_modules/electron-to-chromium": { + "version": "1.5.82", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", + "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==", + "dev": true + }, "node_modules/emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", @@ -3173,6 +3585,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3185,6 +3610,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -3206,6 +3643,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -3246,9 +3689,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -3547,6 +3990,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -3772,6 +4224,12 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "node_modules/glob/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -3808,6 +4266,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4038,6 +4502,25 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4063,6 +4546,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4165,6 +4657,18 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4189,6 +4693,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isomorphic.js": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", @@ -4214,11 +4727,40 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "dev": true + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -4245,6 +4787,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-compare": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", @@ -4338,6 +4886,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -4560,6 +5117,15 @@ "uc.micro": "^2.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4675,6 +5241,12 @@ "node": ">= 0.6" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4907,6 +5479,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/node-addon-api": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", @@ -4916,6 +5494,12 @@ "node": "^16 || ^18 || >= 20" } }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5017,6 +5601,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5140,6 +5733,12 @@ "through2": "^2.0.3" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5152,6 +5751,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/playwright": { "version": "1.47.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", @@ -5395,6 +6058,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5450,6 +6125,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5570,6 +6266,55 @@ } ] }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -5617,6 +6362,18 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5674,6 +6431,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -5814,6 +6590,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar-fs": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", @@ -5849,6 +6634,92 @@ "streamx": "^2.15.0" } }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/text-decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", @@ -5901,6 +6772,35 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -6003,6 +6903,36 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6111,6 +7041,161 @@ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "dev": true }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6126,6 +7211,12 @@ "node": ">= 8" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/package.json b/package.json index 59bd26d448..778066cd41 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "workspaces": [ "./npm/qsharp", "./playground", - "./vscode" + "./vscode", + "./circuit_vis" ], "license": "MIT", "engines": { @@ -19,7 +20,6 @@ "devDependencies": { "@anywidget/types": "^0.1.4", "@eslint/js": "^9.9.1", - "@microsoft/quantum-viz.js": "^1.0.5", "@parcel/watcher": "^2.4.1", "@types/chai": "^4.3.8", "@types/markdown-it": "^14.1.1", @@ -45,10 +45,14 @@ "preact": "^10.20.0", "prettier": "^3.3.3", "punycode": "^2.3.1", + "terser-webpack-plugin": "^5.3.11", + "ts-loader": "^9.5.2", "typescript": "^5.5.4", "typescript-eslint": "^8.19.1", "url": "^0.11.3", "user-agent-data-types": "^0.4.2", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.0.8", + "webpack": "^5.97.1", + "webpack-cli": "^6.0.1" } }