├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── build_test.yml │ └── publish_docs.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── launch.json ├── CHANGELOG.md ├── README.md ├── benchmarks ├── benchmark.ts ├── complexMul.ts └── levels.ts ├── demos └── example.ts ├── imgs └── quantum-tensors-logo.jpg ├── package.json ├── rollup.config.js ├── src ├── Basis.ts ├── Bell.ts ├── Circuit.ts ├── Complex.ts ├── Constants.ts ├── Dimension.ts ├── Elements.ts ├── Entanglement.ts ├── Frame.ts ├── Gates.ts ├── IncoherentLight.ts ├── Measurement.ts ├── Operator.ts ├── OperatorEntry.ts ├── Ops.ts ├── Photons.ts ├── Simulation.ts ├── Vector.ts ├── VectorEntry.ts ├── helpers.ts ├── index.ts └── interfaces.ts ├── tests ├── Basis.test.ts ├── Circuit.test.ts ├── Complex.test.ts ├── Dimension.test.ts ├── Elements.test.ts ├── Entanglement.test.ts ├── Gates.test.ts ├── IncoherentLight.test.ts ├── Measurement.test.ts ├── Operator.test.ts ├── Ops.test.ts ├── Photons.test.ts ├── Simulation.test.ts ├── Vector.test.ts ├── VectorEntry.test.ts ├── customMatchers.ts └── helpers.test.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | insert_final_newline = true 2 | trim_trailing_whitespace = true 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint', 'prettier'], 4 | extends: [ 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:import/errors', 7 | 'plugin:import/warnings', 8 | 'plugin:import/typescript', 9 | 'plugin:prettier/recommended', 10 | ], 11 | settings: { 12 | 'import/parsers': { 13 | '@typescript-eslint/parser': ['.ts', '.tsx'], 14 | }, 15 | 'import/resolver': { 16 | typescript: {}, 17 | }, 18 | }, 19 | rules: { 20 | '@typescript-eslint/no-this-alias': 'off', 21 | '@typescript-eslint/member-delimiter-style': [ 22 | 'off', 23 | { 24 | // Prevents us from using any delimiter for interface properties. 25 | multiline: { 26 | delimiter: 'comma', 27 | requireLast: false, 28 | }, 29 | singleline: { 30 | delimiter: 'comma', 31 | requireLast: false, 32 | }, 33 | }, 34 | ], 35 | 'max-len': ['error', { code: 120 }], 36 | indent: ['error', 2, { SwitchCase: 1 }], 37 | 'comma-dangle': [ 38 | 'error', 39 | { 40 | arrays: 'always-multiline', 41 | objects: 'always-multiline', 42 | imports: 'never', 43 | exports: 'never', 44 | functions: 'only-multiline', 45 | }, 46 | ], 47 | '@typescript-eslint/no-unused-vars': [ 48 | 'error', 49 | { 50 | argsIgnorePattern: '^_', 51 | ignoreRestSiblings: true, 52 | }, 53 | ], 54 | 'require-jsdoc': [ 55 | 1, 56 | { 57 | require: { 58 | FunctionDeclaration: true, 59 | MethodDefinition: false, 60 | ClassDeclaration: true, 61 | ArrowFunctionExpression: false, 62 | FunctionExpression: false, 63 | }, 64 | }, 65 | ], 66 | }, 67 | root: true, 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v2 10 | with: 11 | node-version: '16' 12 | - name: Install 13 | run: yarn 14 | - name: Build 15 | run: yarn build 16 | - name: Test 17 | run: yarn test 18 | - name: Lint 19 | run: yarn lint 20 | - name: Build docs # but not publish 21 | run: yarn build-docs 22 | - uses: codecov/codecov-action@v2 23 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '16' 16 | - name: Install 17 | run: yarn 18 | - name: Build docs 19 | run: yarn build-docs 20 | - name: Jekyll-safe 21 | run: touch docs/.nojekyll 22 | - name: Deploy 23 | uses: JamesIves/github-pages-deploy-action@releases/v3 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | BRANCH: gh-pages 27 | FOLDER: docs 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | lib/ 4 | dist/ 5 | *.log 6 | playground/ 7 | docs/ 8 | coverage 9 | 10 | .DS_Store 11 | 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | endOfLine: 'lf', 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Jest Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand"], 12 | "console": "integratedTerminal", 13 | "internalConsoleOptions": "neverOpen", 14 | "port": 9229 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.9] - 2020-08-24 9 | 10 | Massive performance improvements. On i7-9750H CPU (Coffee Lake): 11 | 12 | * 20x20 board, photon propagation: 22-34ms -> 0.02ms 13 | * 100x100 board, photon propagation: 741 ms -> 0.02 ms 14 | * 20x20 board, 6 operations: 80 ms -> 1.5ms 15 | 16 | ### Added 17 | 18 | * Some performance benchmarks and tests 19 | * Hard-coded photon propagation (a single map, instead of an operator) 20 | * Catching photon operations (rather than recalculating them each step) 21 | * Smarter photon operations (only looking at the difference), i.e. 22 | 23 | ## [0.4.8] - 2020-06-29 24 | 25 | ### Added 26 | 27 | * This CHANGELOG! 28 | * `Operator.permuteDimsOut` and ``Operator.permuteDimsIn` methods. 29 | 30 | ### Fixed 31 | 32 | * Fixed a problem with a dimension check on permuting non-square tensors. 33 | * Set phase for LR basis (for polarization) and y-spin basis (for spin) so that the singlet state phase stays the same 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quantum Tensors 2 | 3 | [![npm version](https://badge.fury.io/js/quantum-tensors.svg)](https://badge.fury.io/js/quantum-tensors) 4 | ![License](https://img.shields.io/npm/l/quantum-tensors) 5 | ![Build](https://github.com/Quantum-Flytrap/quantum-tensors/actions/workflows/build_test.yml/badge.svg) 6 | ![Docs](https://github.com/Quantum-Flytrap/quantum-tensors/actions/workflows/publish_docs.yml/badge.svg) 7 | [![Coverage Status](https://codecov.io/gh/Quantum-Flytrap/quantum-tensors/branch/master/graph/badge.svg)](https://codecov.io/gh/Quantum-Flytrap/quantum-tensors/) 8 | [![Twitter @QuantumFlytrap](https://img.shields.io/twitter/follow/QuantumFlytrap)](https://twitter.com/QuantumFlytrap) 9 | 10 | A TypeScript package for sparse tensor operations on complex numbers in your browser - for quantum computing, quantum information, and interactive visualizations of quantum physics. For more details, see our preprint: 11 | 12 | - P. Migdał, K. Jankiewicz, P. Grabarz, et al., [Visualizing quantum mechanics in an interactive simulation - Virtual Lab by Quantum Flytrap](https://arxiv.org/abs/2203.13300), arXiv:2203.13300 13 | 14 | Quantum Tensors are developed by [Quantum Flytrap](https://quantumflytrap.com/) and were supported by the [Centre of Quantum Technologies](https://www.quantumlah.org/), National University of Singapore. They are a part of the [Quantum Game 2](https://github.com/Quantum-Game/quantum-game-2) project, are used in [BraKetVue](https://github.com/Quantum-Flytrap/bra-ket-vue) quantum state visualizer, and serve as a prototype for numerics for [Virtual Lab by Quantum Flytrap](https://lab.quantumflytrap.com/), and in an interactive blog post: 15 | 16 | - C. Zendejas-Morales, P. Migdał, [Quantum logic gates for a single qubit, interactively](https://quantumflytrap.com/blog/2021/qubit-interactively/) 17 | 18 | Documentation: [quantum-flytrap.github.io/quantum-tensors](https://quantum-flytrap.github.io/quantum-tensors/) (generated by [TypeDoc](https://typedoc.org/)). Some examples of its usage are [demos](https://github.com/Quantum-Flytrap/quantum-tensors/tree/master/demos), [tests](https://github.com/Quantum-Flytrap/quantum-tensors/tree/master/demos) folders. 19 | 20 | We base the philosophy of this package on: 21 | 22 | - Sparse operations (both for vectors and matrices) 23 | - Complex numbers 24 | - Tensor structure 25 | - Named tensor dimensions (vide [Tensors considered harmful](http://nlp.seas.harvard.edu/NamedTensor)): there is a difference between a 2x2 operator on spin and polarization. It helps with catching errors. 26 | 27 | A few insights on contributing, and starting your projects, are in [How I Learned to Stop Worrying and Love the Types & Tests: the Zen of Python for TypeScript](https://p.migdal.pl/2020/03/02/types-tests-typescript.html) by Piotr Migdał. 28 | 29 | ![Quantum Tensors logo](imgs/quantum-tensors-logo.jpg) 30 | 31 | ## Installation 32 | 33 | The easiest way is to install from [the NPM repository](https://www.npmjs.com/package/quantum-tensors): 34 | 35 | ``` 36 | npm install quantum-tensors 37 | ``` 38 | 39 | Or, if you use [yarn](https://yarnpkg.com/) package manager, 40 | 41 | ``` 42 | yarn add quantum-tensors 43 | ``` 44 | 45 | If you want to install a development version, you can get this package directly from this GitHub repository. In this case, the commands are: 46 | 47 | ``` 48 | npm install Quantum-Flytrap/quantum-tensors#master 49 | ``` 50 | 51 | Or if you use yarn: 52 | 53 | ``` 54 | yarn add Quantum-Flytrap/quantum-tensors#master 55 | ``` 56 | 57 | ## Usage 58 | 59 | And then in your project write: 60 | 61 | ```{ts} 62 | import * as qt from 'quantum-tensors' 63 | ``` 64 | 65 | Also, [https://github.com/stared/thinking-in-tensors-writing-in-pytorch](https://github.com/stared/thinking-in-tensors-writing-in-pytorch) by Piotr Migdał. 66 | 67 | ## Contributing 68 | 69 | - Create [TSDoc](https://www.npmjs.com/package/@microsoft/tsdoc) for each new function, class, and method 70 | - Setup [ESLint](https://eslint.org/) configured as in this `.eslintrc.js` 71 | - Try to adhere to [Angular-like commit style](https://github.com/angular/angular/blob/master/CONTRIBUTING.md) 72 | 73 | ## Citing 74 | 75 | - P. Migdał, K. Jankiewicz, P. Grabarz, et al., [Visualizing quantum mechanics in an interactive simulation - Virtual Lab by Quantum Flytrap](https://arxiv.org/abs/2203.13300), arXiv:2203.13300 76 | 77 | ``` 78 | @article{migdal_visualizing_2022, 79 | title = {Visualizing quantum mechanics in an interactive simulation -- {Virtual} {Lab} by {Quantum} {Flytrap}}, 80 | url = {http://arxiv.org/abs/2203.13300}, 81 | journal = {arXiv:2203.13300 [quant-ph]}, 82 | author = {Migdał, Piotr and Jankiewicz, Klementyna and Grabarz, Paweł and Decaroli, Chiara and Cochin, Philippe}, 83 | month = mar, 84 | year = {2022}, 85 | note = {arXiv: 2203.13300} 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /benchmarks/benchmark.ts: -------------------------------------------------------------------------------- 1 | // See infos at https://nodejs.org/api/perf_hooks.html#perf_hooks_performance_now 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const { performance } = require('perf_hooks') 4 | import Simulation, { generateLaserIndicator } from '../src/Simulation' 5 | import { grid1, grid2 } from './levels' 6 | 7 | console.log('---Running benchmarks---') 8 | let sim = Simulation.fromGrid(grid1) 9 | sim.initializeFromIndicator(generateLaserIndicator(grid1.cells)) 10 | const t0 = performance.now() 11 | sim.generateFrames(100) 12 | const t1 = performance.now() 13 | console.log(`Grid 1 (100 frames) took ${t1 - t0} milliseconds.`) 14 | 15 | sim = Simulation.fromGrid(grid2) 16 | sim.initializeFromIndicator(generateLaserIndicator(grid2.cells)) 17 | const t2 = performance.now() 18 | sim.generateFrames(100) 19 | const t3 = performance.now() 20 | console.log(`Grid 2 (100 frames) took ${t3 - t2} milliseconds.`) 21 | -------------------------------------------------------------------------------- /benchmarks/complexMul.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const b = require('benny') 3 | import Complex from '../src/Complex' 4 | 5 | /** 6 | * Random float generator 7 | */ 8 | function randFloat(min = -10000, max = 10000): number { 9 | return (Math.random() * (max - min) + min) / 100 10 | } 11 | const z1 = new Complex(randFloat(), randFloat()) 12 | const z2 = new Complex(randFloat(), randFloat()) 13 | 14 | // add tests 15 | b.suite( 16 | 'ComplexMul algorithms', 17 | 18 | b.add('ComplexMultiply#Naive', function () { 19 | z1.mul(z2) 20 | }), 21 | 22 | b.add('ComplexMultiply#Gauss', function () { 23 | z1.mulGauss(z2) 24 | }), 25 | 26 | b.cycle(), 27 | 28 | b.complete(), 29 | ) 30 | -------------------------------------------------------------------------------- /benchmarks/levels.ts: -------------------------------------------------------------------------------- 1 | import { IGrid } from '../src/interfaces' 2 | 3 | export const grid1: IGrid = { 4 | cols: 13, 5 | rows: 10, 6 | cells: [ 7 | { 8 | y: 0, 9 | x: 0, 10 | element: 'Mirror', 11 | rotation: 45, 12 | polarization: 0, 13 | }, 14 | { 15 | y: 0, 16 | x: 12, 17 | element: 'Mirror', 18 | rotation: 135, 19 | polarization: 0, 20 | }, 21 | { 22 | y: 2, 23 | x: 2, 24 | element: 'Mirror', 25 | rotation: 45, 26 | polarization: 0, 27 | }, 28 | { 29 | y: 2, 30 | x: 3, 31 | element: 'Mirror', 32 | rotation: 135, 33 | polarization: 0, 34 | }, 35 | { 36 | y: 3, 37 | x: 1, 38 | element: 'Laser', 39 | rotation: 0, 40 | polarization: 0, 41 | }, 42 | { 43 | y: 3, 44 | x: 2, 45 | element: 'BeamSplitter', 46 | rotation: 135, 47 | polarization: 0, 48 | }, 49 | { 50 | y: 3, 51 | x: 3, 52 | element: 'Mirror', 53 | rotation: 45, 54 | polarization: 0, 55 | }, 56 | { 57 | y: 2, 58 | x: 0, 59 | element: 'Detector', 60 | rotation: 90, 61 | polarization: 0, 62 | }, 63 | { 64 | y: 9, 65 | x: 2, 66 | element: 'Mirror', 67 | rotation: 135, 68 | polarization: 0, 69 | }, 70 | { 71 | y: 9, 72 | x: 12, 73 | element: 'Mirror', 74 | rotation: 45, 75 | polarization: 0, 76 | }, 77 | ], 78 | } 79 | 80 | export const grid2: IGrid = { 81 | cols: 13, 82 | rows: 10, 83 | cells: [ 84 | { 85 | y: 2, 86 | x: 6, 87 | element: 'Mirror', 88 | rotation: 0, 89 | polarization: 0, 90 | }, 91 | { 92 | y: 2, 93 | x: 7, 94 | element: 'Mirror', 95 | rotation: 0, 96 | polarization: 0, 97 | }, 98 | { 99 | y: 2, 100 | x: 8, 101 | element: 'Mirror', 102 | rotation: 0, 103 | polarization: 0, 104 | }, 105 | { 106 | y: 3, 107 | x: 1, 108 | element: 'Laser', 109 | rotation: 0, 110 | polarization: 0, 111 | }, 112 | { 113 | y: 4, 114 | x: 1, 115 | element: 'SugarSolution', 116 | rotation: 0, 117 | polarization: 0, 118 | }, 119 | { 120 | y: 4, 121 | x: 2, 122 | element: 'SugarSolution', 123 | rotation: 0, 124 | polarization: 0, 125 | }, 126 | { 127 | y: 3, 128 | x: 4, 129 | element: 'PolarizingBeamSplitter', 130 | rotation: 180, 131 | polarization: 0, 132 | }, 133 | { 134 | y: 3, 135 | x: 5, 136 | element: 'FaradayRotator', 137 | rotation: 180, 138 | polarization: 0, 139 | }, 140 | { 141 | y: 3, 142 | x: 6, 143 | element: 'BeamSplitter', 144 | rotation: 135, 145 | polarization: 0, 146 | }, 147 | { 148 | y: 3, 149 | x: 7, 150 | element: 'BeamSplitter', 151 | rotation: 135, 152 | polarization: 0, 153 | }, 154 | { 155 | y: 3, 156 | x: 8, 157 | element: 'BeamSplitter', 158 | rotation: 135, 159 | polarization: 0, 160 | }, 161 | { 162 | y: 3, 163 | x: 9, 164 | element: 'Mirror', 165 | rotation: 90, 166 | polarization: 0, 167 | }, 168 | { 169 | y: 4, 170 | x: 5, 171 | element: 'Mirror', 172 | rotation: 90, 173 | polarization: 0, 174 | }, 175 | { 176 | y: 4, 177 | x: 6, 178 | element: 'BeamSplitter', 179 | rotation: 135, 180 | polarization: 0, 181 | }, 182 | { 183 | y: 4, 184 | x: 7, 185 | element: 'BeamSplitter', 186 | rotation: 135, 187 | polarization: 0, 188 | }, 189 | { 190 | y: 4, 191 | x: 8, 192 | element: 'BeamSplitter', 193 | rotation: 135, 194 | polarization: 0, 195 | }, 196 | { 197 | y: 4, 198 | x: 9, 199 | element: 'Mirror', 200 | rotation: 90, 201 | polarization: 0, 202 | }, 203 | { 204 | y: 5, 205 | x: 5, 206 | element: 'Mirror', 207 | rotation: 90, 208 | polarization: 0, 209 | }, 210 | { 211 | y: 5, 212 | x: 6, 213 | element: 'BeamSplitter', 214 | rotation: 135, 215 | polarization: 0, 216 | }, 217 | { 218 | y: 5, 219 | x: 7, 220 | element: 'BeamSplitter', 221 | rotation: 135, 222 | polarization: 0, 223 | }, 224 | { 225 | y: 5, 226 | x: 8, 227 | element: 'BeamSplitter', 228 | rotation: 135, 229 | polarization: 0, 230 | }, 231 | { 232 | y: 5, 233 | x: 4, 234 | element: 'Detector', 235 | rotation: 90, 236 | polarization: 0, 237 | }, 238 | { 239 | y: 6, 240 | x: 6, 241 | element: 'Mirror', 242 | rotation: 0, 243 | polarization: 0, 244 | }, 245 | { 246 | y: 6, 247 | x: 7, 248 | element: 'Mirror', 249 | rotation: 0, 250 | polarization: 0, 251 | }, 252 | { 253 | y: 6, 254 | x: 8, 255 | element: 'Mirror', 256 | rotation: 0, 257 | polarization: 0, 258 | }, 259 | { 260 | y: 5, 261 | x: 9, 262 | element: 'Mirror', 263 | rotation: 90, 264 | polarization: 0, 265 | }, 266 | ], 267 | } 268 | -------------------------------------------------------------------------------- /demos/example.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import Vector from '../src/Vector' 4 | import Operator from '../src/Operator' 5 | 6 | // const vectorComplex = [new Complex(1, 0), new Complex(0, 0), new Complex(2, 1), new Complex(0, -1)] 7 | const dim1 = Dimension.direction() 8 | const dim2 = Dimension.spin() 9 | const vector1 = Vector.fromArray([Cx(1), Cx(0), Cx(2, 1), Cx(0, -1)], [dim1], false) 10 | const vector2 = Vector.fromArray([Cx(0, 0.5), Cx(1)], [dim2], false) 11 | console.log(vector1.toString()) 12 | console.log(vector2.toString()) 13 | 14 | console.log('Vector from sparse names') 15 | const vecFromSparse = Vector.fromSparseCoordNames( 16 | [ 17 | ['uH', Cx(0, 2)], 18 | ['dH', Cx(-1, -1)], 19 | ['dV', Cx(0.5, 2.5)], 20 | ], 21 | [Dimension.spin(), Dimension.polarization()], 22 | ) 23 | console.log(vecFromSparse.toString()) 24 | 25 | // Permutations 26 | console.log('No perm') 27 | console.log(vecFromSparse.permute([0, 1]).toString()) 28 | 29 | console.log('Perm swap') 30 | console.log(vecFromSparse.permute([1, 0]).toString()) 31 | 32 | // console.log('Wrong perm') 33 | // console.log(vecFromSparse.permute([0]).toString()) 34 | 35 | // Outer product 36 | const outerVec = vector1.outer(vector2) 37 | console.log(outerVec.toString('cartesian', 2, '\n')) 38 | console.log(outerVec.toString('polar', 2, '\n')) 39 | console.log(outerVec.toString('polarTau', 2, '\n')) 40 | 41 | // Add 42 | console.log('Add') 43 | const vector3 = Vector.fromArray([Cx(0, 1), Cx(0), Cx(-1, 2), Cx(0)], [dim1], false) 44 | console.log(vector1.toString()) 45 | console.log(vector3.toString()) 46 | console.log(vector1.add(vector3).toString()) 47 | 48 | // Dot 49 | console.log('Dot') 50 | console.log(vector1.dot(vector3).toString()) 51 | 52 | // Conj 53 | console.log(vector1.conj().toString()) 54 | 55 | // Operators 56 | console.log('Operators') 57 | 58 | console.log('Identity') 59 | const idPolDir = Operator.identity([Dimension.polarization(), Dimension.direction()]) 60 | console.log(idPolDir.toString()) 61 | 62 | console.log('Pieces and a tensor product') 63 | const idPol = Operator.identity([Dimension.polarization()]) 64 | console.log(idPol.toString()) 65 | const idDir = Operator.identity([Dimension.spin()]) 66 | console.log(idDir.toString()) 67 | 68 | console.log(idPol.outer(idDir).toString()) 69 | console.log(idDir.outer(idPol).toString()) 70 | 71 | console.log('From array') 72 | const spinY = Operator.fromArray( 73 | [ 74 | [Cx(0), Cx(0, -1)], 75 | [Cx(0, 1), Cx(0)], 76 | ], 77 | [Dimension.spin()], 78 | ) 79 | console.log(spinY.toString()) 80 | const spinX = Operator.fromArray( 81 | [ 82 | [Cx(0), Cx(1)], 83 | [Cx(1), Cx(0)], 84 | ], 85 | [Dimension.spin()], 86 | ) 87 | console.log(spinX.toString()) 88 | 89 | console.log('From sparse names') 90 | const opFromSparse = Operator.fromSparseCoordNames( 91 | [ 92 | ['uH', 'uH', Cx(0, 2)], 93 | ['dH', 'uV', Cx(-1, -1)], 94 | ['dV', 'dH', Cx(0.5, 2.5)], 95 | ], 96 | [Dimension.spin(), Dimension.polarization()], 97 | ) 98 | console.log(opFromSparse.toString()) 99 | 100 | console.log('Tensor spinY and spinX') 101 | console.log(spinY.outer(spinX).toString()) 102 | 103 | console.log('Op vec mul') 104 | console.log(vector2.toString()) 105 | console.log(spinY.mulVec(vector2).toString()) 106 | console.log(spinX.mulVec(vector2).toString()) 107 | 108 | console.log('Hadamard') 109 | const isrt2 = 1 / Math.sqrt(2) 110 | const spinH = Operator.fromArray( 111 | [ 112 | [Cx(isrt2), Cx(isrt2)], 113 | [Cx(isrt2), Cx(-isrt2)], 114 | ], 115 | [Dimension.spin()], 116 | ) 117 | console.log(spinH.toString()) 118 | console.log(spinH.mulVec(vector2).toString()) 119 | -------------------------------------------------------------------------------- /imgs/quantum-tensors-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantum-Flytrap/quantum-tensors/b8c677c51475ba33644cb62658796b5a190fdf42/imgs/quantum-tensors-logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quantum-tensors", 3 | "version": "0.4.15", 4 | "description": "Tensor numerics for quantum information and quantum optics. Used in Quantum Game with Photons.", 5 | "main": "dist/quantum-tensors.js", 6 | "module": "dist/quantum-tensors.esm.js", 7 | "unpkg": "dist/quantum-tensors.min.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "README.md", 12 | "imgs" 13 | ], 14 | "repository": "https://github.com/Quantum-Flytrap/quantum-tensors", 15 | "author": "Piotr Migdal", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@rollup/plugin-commonjs": "^21.0.3", 19 | "@rollup/plugin-node-resolve": "^13.1.3", 20 | "@types/jest": "^27.4.1", 21 | "@typescript-eslint/eslint-plugin": "^5.17.0", 22 | "@typescript-eslint/parser": "^5.17.0", 23 | "add": "^2.0.6", 24 | "benny": "^3.7.1", 25 | "eslint": "^8.12.0", 26 | "eslint-config-airbnb": "^19.0.4", 27 | "eslint-config-prettier": "^8.5.0", 28 | "eslint-import-resolver-typescript": "^2.7.0", 29 | "eslint-plugin-import": "^2.25.4", 30 | "eslint-plugin-json": "^3.1.0", 31 | "eslint-plugin-jsx-a11y": "^6.5.1", 32 | "eslint-plugin-prettier": "^4.0.0", 33 | "eslint-plugin-react": "^7.29.4", 34 | "jest": "^27.5.1", 35 | "prettier": "^2.6.1", 36 | "rollup": "^2.70.1", 37 | "rollup-plugin-terser": "^7.0.2", 38 | "rollup-plugin-typescript2": "^0.31.2", 39 | "ts-jest": "^27.1.4", 40 | "typedoc": "^0.22.13", 41 | "typescript": "^4.6.3", 42 | "yarn-upgrade-all": "^0.7.0" 43 | }, 44 | "scripts": { 45 | "benchmark": "ts-node ./benchmarks/benchmark.ts", 46 | "build-old": "tsc --pretty", 47 | "build": "rollup -c && tsc", 48 | "test": "tsc --noEmit -p . && jest --coverage --no-cache", 49 | "test-sim": "tsc --noEmit -p . && jest --no-cache ./tests/Simulation.test.ts", 50 | "build-docs": "typedoc --out docs src", 51 | "build-and-deploy-docs": "typedoc --out docs src && touch docs/.nojekyll && push-dir --dir=docs --branch=gh-pages --cleanup", 52 | "build-defs": "tsc --declaration --outDir . --emitDeclarationOnly", 53 | "eslint-fix": "eslint --fix src/*.ts", 54 | "lint": "eslint src/*.ts", 55 | "prepare": "npm run build", 56 | "prepublishOnly": "npm run lint && npm run test", 57 | "postversion": "git push && git push --tags && npm run build-and-deploy-docs" 58 | }, 59 | "dependencies": { 60 | "@types/lodash": "^4.14.181", 61 | "lodash": "^4.17.21" 62 | }, 63 | "jest": { 64 | "transform": { 65 | ".(ts|tsx)": "ts-jest" 66 | }, 67 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$", 68 | "moduleFileExtensions": [ 69 | "ts", 70 | "js" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import pkg from './package.json' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | // delete old typings to avoid issues 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | require('fs').unlink('dist/src/index.d.ts', () => {}) 10 | 11 | export default { 12 | input: 'src/index.ts', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | name: 'QuantumTensors', 18 | compact: true, 19 | exports: 'named', 20 | }, 21 | { 22 | file: pkg.module, 23 | format: 'esm', 24 | name: 'QuantumTensors', 25 | exports: 'named', 26 | }, 27 | { 28 | file: pkg.unpkg, 29 | format: 'iife', 30 | name: 'QuantumTensors', 31 | compact: true, 32 | exports: 'named', 33 | }, 34 | ], 35 | plugins: [ 36 | commonjs(), 37 | resolve(), 38 | typescript({ 39 | typescript: require('typescript'), 40 | }), 41 | terser(), 42 | ], 43 | } 44 | -------------------------------------------------------------------------------- /src/Basis.ts: -------------------------------------------------------------------------------- 1 | import Complex, { Cx } from './Complex' 2 | import Vector from './Vector' 3 | import Operator from './Operator' 4 | import Dimension from './Dimension' 5 | import { INamedVector } from './interfaces' 6 | 7 | /** 8 | * Class for dealing with bases. 9 | */ 10 | export default class Basis { 11 | namedVectors: INamedVector[] 12 | 13 | /** 14 | * As most constructors, for intenal use only. 15 | * @param namedVectorValues 16 | * @param dimension 17 | */ 18 | constructor(namedVectorValues: { name: string; values: Complex[] }[], computationalDimension: Dimension) { 19 | if (namedVectorValues.length !== computationalDimension.size) { 20 | throw new Error( 21 | `There are ${namedVectorValues.length} vectors` + 22 | ` - incorrect for a basis for ${computationalDimension.toString()}`, 23 | ) 24 | } 25 | if (namedVectorValues.length !== computationalDimension.size) { 26 | throw new Error( 27 | `There are ${namedVectorValues.length} vectors` + 28 | ` - incorrect for a basis for ${computationalDimension.toString()}`, 29 | ) 30 | } 31 | this.namedVectors = namedVectorValues.map((d) => ({ 32 | name: d.name, 33 | vector: Vector.fromArray(d.values, [computationalDimension]).normalize(), 34 | })) 35 | } 36 | 37 | /** 38 | * Get dimension. 39 | */ 40 | get computationalDimension(): Dimension { 41 | return this.namedVectors[0].vector.dimensions[0] 42 | } 43 | 44 | get basisCoordNames(): string[] { 45 | return this.namedVectors.map((d) => d.name) 46 | } 47 | 48 | /** 49 | * Get basis string. 50 | */ 51 | get basisStr(): string { 52 | return this.basisCoordNames.join('') 53 | } 54 | 55 | get basisDimension(): Dimension { 56 | return this.computationalDimension.reassignCoordNames(this.basisCoordNames) 57 | } 58 | 59 | /** 60 | * Generates a string. 61 | */ 62 | toString(): string { 63 | const intro = `Basis ${this.basisStr} for dimension ${this.computationalDimension.name}` 64 | const listStr = this.namedVectors.map((d) => `|${d.name}⟩ = ${d.vector.toKetString('cartesian')}`) 65 | return `${intro}\n${listStr.join('\n')}` 66 | } 67 | 68 | /** 69 | * Bases for polarization {@link Dimension.polarization} 70 | * @param basisStr 'HV' for horizontal, 'DA' for diagonal, 'LR' for circular 71 | */ 72 | static polarization(basisStr: string): Basis { 73 | switch (basisStr) { 74 | case 'HV': 75 | return new Basis( 76 | [ 77 | { name: 'H', values: [Cx(1), Cx(0)] }, 78 | { name: 'V', values: [Cx(0), Cx(1)] }, 79 | ], 80 | Dimension.polarization(), 81 | ) 82 | case 'DA': 83 | return new Basis( 84 | [ 85 | { name: 'D', values: [Cx(1), Cx(1)] }, 86 | { name: 'A', values: [Cx(-1), Cx(1)] }, 87 | ], 88 | Dimension.polarization(), 89 | ) 90 | case 'LR': 91 | return new Basis( 92 | [ 93 | { name: 'L', values: [Cx(1), Cx(0, 1)] }, 94 | { name: 'R', values: [Cx(0, 1), Cx(1)] }, 95 | ], 96 | Dimension.polarization(), 97 | ) 98 | default: 99 | throw new Error(`Basis ${basisStr} not aviable bases for polarization ['HV', 'DA', 'LR'].`) 100 | } 101 | } 102 | 103 | /** 104 | * Bases for qubit {@link Dimension.qubit} 105 | * @note Different from polarization, as |-⟩ ~ |0⟩ - |1⟩. 106 | * @note |i+⟩ and |i-⟩ will test the ground for multichar coord names. 107 | * @param basisStr '01' for computational, '+-' for diagonal, '+i-i' for circular 108 | */ 109 | static qubit(basisStr: string): Basis { 110 | switch (basisStr) { 111 | case '01': 112 | return new Basis( 113 | [ 114 | { name: '0', values: [Cx(1), Cx(0)] }, 115 | { name: '1', values: [Cx(0), Cx(1)] }, 116 | ], 117 | Dimension.qubit(), 118 | ) 119 | case '+-': 120 | return new Basis( 121 | [ 122 | { name: '+', values: [Cx(1), Cx(1)] }, 123 | { name: '-', values: [Cx(1), Cx(-1)] }, 124 | ], 125 | Dimension.qubit(), 126 | ) 127 | case '+i-i': 128 | return new Basis( 129 | [ 130 | { name: '+i', values: [Cx(1), Cx(0, 1)] }, 131 | { name: '-i', values: [Cx(1), Cx(0, -1)] }, 132 | ], 133 | Dimension.qubit(), 134 | ) 135 | default: 136 | throw new Error(`Basis ${basisStr} not aviable bases for qubits ['01', '+-', '+i-i'].`) 137 | } 138 | } 139 | 140 | /** 141 | * Bases for spin-1/2 {@link Dimension.spin} 142 | * @note Let's test multiple names. 143 | * @param basisStr 'ud' or 'spin-z' or 'uzdz' for z, 'spin-x' or 'uxdx', 'spin-y' or 'uydy' 144 | */ 145 | static spin(basisStr: string): Basis { 146 | switch (basisStr) { 147 | case 'spin-z': 148 | case 'ud': 149 | case 'uzdz': 150 | return new Basis( 151 | [ 152 | { name: 'u', values: [Cx(1), Cx(0)] }, 153 | { name: 'd', values: [Cx(0), Cx(1)] }, 154 | ], 155 | Dimension.spin(), 156 | ) 157 | case 'spin-x': 158 | case 'uxdx': 159 | return new Basis( 160 | [ 161 | { name: 'ux', values: [Cx(1), Cx(1)] }, 162 | { name: 'dx', values: [Cx(-1), Cx(1)] }, 163 | ], 164 | Dimension.spin(), 165 | ) 166 | case 'spin-y': 167 | case 'uydy': 168 | return new Basis( 169 | [ 170 | { name: 'uy', values: [Cx(1), Cx(0, 1)] }, 171 | { name: 'dy', values: [Cx(0, 1), Cx(1)] }, 172 | ], 173 | Dimension.spin(), 174 | ) 175 | default: 176 | throw new Error(`Basis ${basisStr} not aviable bases for spin.`) 177 | } 178 | } 179 | 180 | /** 181 | * A shorthand method for {@link Basis.polarization}, {@link Basis.spin} and {@link Basis.qubit}. 182 | * @param dimName One of: ['polarization', 'spin', 'qubit'] 183 | * @param basisStr Basis string. 184 | */ 185 | static fromString(dimName: string, basisStr: string): Basis { 186 | switch (dimName) { 187 | case 'polarization': 188 | return Basis.polarization(basisStr) 189 | case 'spin': 190 | return Basis.spin(basisStr) 191 | case 'qubit': 192 | return Basis.qubit(basisStr) 193 | default: 194 | throw new Error(`Basis.fromString: dimName ${dimName} not in ['polarization', 'spin', 'qubit'].`) 195 | } 196 | } 197 | 198 | static basisChangeU(basisTo: Basis, basisFrom: Basis): Operator { 199 | const entries = basisTo.namedVectors.flatMap((to) => 200 | basisFrom.namedVectors.map((from): [string[], string[], Complex] => { 201 | return [[to.name], [from.name], to.vector.inner(from.vector)] 202 | }), 203 | ) 204 | return Operator.fromSparseCoordNames(entries, [basisTo.basisDimension], [basisFrom.basisDimension]) 205 | } 206 | 207 | basisChangeUnitary(basisFrom: Basis): Operator { 208 | return Basis.basisChangeU(this, basisFrom) 209 | } 210 | 211 | basisChangeUnitaryFromDimension(dimension: Dimension): Operator { 212 | const basisStr = dimension.coordNames.join('') 213 | switch (dimension.name) { 214 | case 'polarization': 215 | return this.basisChangeUnitary(Basis.polarization(basisStr)) 216 | case 'qubit': 217 | return this.basisChangeUnitary(Basis.qubit(basisStr)) 218 | case 'spin': 219 | return this.basisChangeUnitary(Basis.spin(basisStr)) 220 | default: 221 | throw new Error(`Basis change not yet implemented for ${dimension}.`) 222 | } 223 | } 224 | 225 | changeAllBasesUnitary(dimensions: Dimension[]): Operator { 226 | const ops = dimensions.map((dimension) => { 227 | if (dimension.name !== this.basisDimension.name) { 228 | return Operator.identity([dimension]) 229 | } else { 230 | return this.basisChangeUnitaryFromDimension(dimension) 231 | } 232 | }) 233 | return Operator.outer(ops) 234 | } 235 | 236 | changeAllDimsOfVector(vector: Vector): Vector { 237 | return this.changeAllBasesUnitary(vector.dimensions).mulVec(vector) 238 | } 239 | 240 | changeAllDimsOfOperator(operator: Operator): Operator { 241 | const changeOut = this.changeAllBasesUnitary(operator.dimensionsOut) 242 | const changeIn = this.changeAllBasesUnitary(operator.dimensionsIn).dag() 243 | return changeOut.mulOp(operator).mulOp(changeIn) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Bell.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from './Complex' 2 | import Dimension from './Dimension' 3 | import Vector from './Vector' 4 | import Operator from './Operator' 5 | import { DEG_TO_RAD } from './Constants' 6 | 7 | // takem from Quantum Boxing 8 | // https://github.com/sneakyweasel/quantum-boxing 9 | // and maybe it can come back! 10 | 11 | // TODO in main package: 12 | // - jest 13 | // - operators X, Y, Z, etc 14 | // - how many non-zero entries in toString method 15 | // - rename tensor dims 16 | // - base change 17 | 18 | // TODO HERE: 19 | // - application 20 | // - measurement 21 | // - some formula view 22 | 23 | const dimPol = Dimension.polarization() 24 | 25 | export const singletState = Vector.fromSparseCoordNames( 26 | [ 27 | ['HV', Cx(1)], 28 | ['VH', Cx(-1)], 29 | ], 30 | [dimPol, dimPol], 31 | ) 32 | 33 | export const opI = Operator.identity([dimPol]) 34 | 35 | export const opX = Operator.fromSparseCoordNames( 36 | [ 37 | ['V', 'H', Cx(1)], 38 | ['H', 'V', Cx(1)], 39 | ], 40 | [dimPol], 41 | ) 42 | 43 | export const opY = Operator.fromSparseCoordNames( 44 | [ 45 | ['V', 'H', Cx(0, 1)], 46 | ['H', 'V', Cx(0, -1)], 47 | ], 48 | [dimPol], 49 | ) 50 | 51 | export const opZ = Operator.fromSparseCoordNames( 52 | [ 53 | ['H', 'H', Cx(1)], 54 | ['V', 'V', Cx(-1)], 55 | ], 56 | [dimPol], 57 | ) 58 | 59 | /** 60 | * Creates a lineart polarized state 61 | * @param alpha Angle for detector (in degrees) 62 | */ 63 | function linearPol(alpha: number): Vector { 64 | return Vector.fromSparseCoordNames( 65 | [ 66 | ['H', Cx(Math.cos(alpha * DEG_TO_RAD))], 67 | ['V', Cx(Math.sin(alpha * DEG_TO_RAD))], 68 | ], 69 | [dimPol], 70 | ) 71 | } 72 | 73 | /** 74 | * Mesure one qubit 75 | * @param alpha Angle of detector (in degrees) 76 | * @param vec Vector to be measured 77 | */ 78 | function measurementOne(alpha: number, vec: Vector): [number, number] { 79 | const res = linearPol(alpha).conj().dot(vec).abs2() 80 | return [res, 1 - res] 81 | } 82 | 83 | /** 84 | * Formats probability as percent string. 85 | * @param p number 0 to 1 86 | */ 87 | function perc(p: number): string { 88 | return `${(100 * p).toFixed(0)}%` 89 | } 90 | 91 | // testing 92 | const ourState = linearPol(45) 93 | const angles: number[] = [-45, 0, 45, 90, 135] 94 | angles.forEach((alpha) => { 95 | const [res, opRes] = measurementOne(alpha, ourState) 96 | console.log(`At ${alpha} the result was: ${perc(res)} vs ${perc(opRes)}`) 97 | }) 98 | 99 | // TO DO measumementOneOfTwo(alpha: number, particle: number) 100 | -------------------------------------------------------------------------------- /src/Circuit.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from './Complex' 2 | import Dimension from './Dimension' 3 | import VectorEntry from './VectorEntry' 4 | import Vector from './Vector' 5 | import Operator from './Operator' 6 | import * as Gates from './Gates' 7 | 8 | const qubit0 = Vector.fromArray([Cx(1), Cx(0)], [Dimension.qubit()]) 9 | const qubit1 = Vector.fromArray([Cx(0), Cx(1)], [Dimension.qubit()]) 10 | 11 | interface IMeasurementResult { 12 | measured: string 13 | probability: number 14 | newState: S 15 | } 16 | 17 | /** 18 | * Represent the present state of a quantum computing circuit. 19 | */ 20 | export default class Circuit { 21 | vector: Vector 22 | qubitCounter: number 23 | qubitIds: number[] 24 | 25 | constructor(vector: Vector, qubitCounter: number, qubitIds: number[]) { 26 | this.vector = vector 27 | this.qubitCounter = qubitCounter 28 | this.qubitIds = qubitIds 29 | } 30 | 31 | static empty(): Circuit { 32 | return new Circuit(new Vector([new VectorEntry([], Cx(1))], []), 0, []) 33 | } 34 | 35 | static qubits(n: number): Circuit { 36 | const qubitIds = [...new Array(n)].map((x, i) => i) 37 | const vector = new Vector( 38 | [ 39 | new VectorEntry( 40 | qubitIds.map(() => 0), 41 | Cx(1), 42 | ), 43 | ], 44 | qubitIds.map(() => Dimension.qubit()), 45 | ) 46 | return new Circuit(vector, n, qubitIds) 47 | } 48 | 49 | addQubit(): Circuit { 50 | this.vector = this.vector.outer(qubit0) 51 | this.qubitIds.push(this.qubitCounter) 52 | this.qubitCounter += 1 53 | return this 54 | } 55 | 56 | applyGate(gate: Operator, at: number[]): Circuit { 57 | // we need to map at to the current qubitIds 58 | this.vector = gate.mulVecPartial(at, this.vector) 59 | return this 60 | } 61 | 62 | copy(): Circuit { 63 | return new Circuit(this.vector.copy(), this.qubitCounter, [...this.qubitIds]) 64 | } 65 | 66 | saveTo(history: Circuit[]): Circuit { 67 | history.push(this.copy()) 68 | return this 69 | } 70 | 71 | measureQubit(at: number): IMeasurementResult[] { 72 | const newQubitIds = this.qubitIds.filter((i) => i !== at) 73 | const vector0 = qubit0.innerPartial([at], this.vector) 74 | const vector1 = qubit1.innerPartial([at], this.vector) 75 | return [ 76 | { 77 | measured: '0', 78 | probability: vector0.normSquared(), 79 | newState: new Circuit(vector0, this.qubitCounter, [...newQubitIds]), 80 | }, 81 | { 82 | measured: '1', 83 | probability: vector1.normSquared(), 84 | newState: new Circuit(vector1, this.qubitCounter, [...newQubitIds]), 85 | }, 86 | ] 87 | } 88 | 89 | // all gates 90 | 91 | X(i: number): Circuit { 92 | return this.applyGate(Gates.X(), [i]) 93 | } 94 | 95 | Y(i: number): Circuit { 96 | return this.applyGate(Gates.Y(), [i]) 97 | } 98 | 99 | Z(i: number): Circuit { 100 | return this.applyGate(Gates.Z(), [i]) 101 | } 102 | 103 | H(i: number): Circuit { 104 | return this.applyGate(Gates.H(), [i]) 105 | } 106 | 107 | /** 108 | * Apply the Pauli I gate 109 | * 110 | * @param i - position of the qubit to apply the gate 111 | * @returns circuit with the Pauli I gate applied 112 | */ 113 | I(i: number): Circuit { 114 | return this.applyGate(Gates.I(), [i]) 115 | } 116 | 117 | /** 118 | * Apply the S gate 119 | * 120 | * @see https://en.wikipedia.org/wiki/Quantum_logic_gate 121 | * 122 | * @param i - position of the qubit to apply the gate 123 | * @returns circuit with the S gate applied 124 | */ 125 | S(i: number): Circuit { 126 | return this.applyGate(Gates.S(), [i]) 127 | } 128 | 129 | /** 130 | * Apply the T gate 131 | * 132 | * @see https://en.wikipedia.org/wiki/Quantum_logic_gate 133 | * 134 | * @param i - position of the qubit to apply the gate 135 | * @returns circuit with the T gate applied 136 | */ 137 | T(i: number): Circuit { 138 | return this.applyGate(Gates.T(), [i]) 139 | } 140 | 141 | CNOT(control: number, target: number): Circuit { 142 | return this.applyGate(Gates.CX(), [control, target]) 143 | } 144 | 145 | SWAP(i: number, j: number): Circuit { 146 | return this.applyGate(Gates.Swap(), [i, j]) 147 | } 148 | 149 | TOFFOLI(control1: number, control2: number, target: number): Circuit { 150 | return this.applyGate(Gates.CCX(), [control1, control2, target]) 151 | } 152 | 153 | FREDKIN(control: number, target1: number, target2: number): Circuit { 154 | return this.applyGate(Gates.CSwap(), [control, target1, target2]) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Complex.ts: -------------------------------------------------------------------------------- 1 | import { TAU } from './Constants' 2 | import { hslToHex } from './helpers' 3 | 4 | export type ComplexFormat = 'cartesian' | 'polar' | 'polarTau' 5 | 6 | /** 7 | * Complex number class 8 | * https://en.wikipedia.org/wiki/Complex_number 9 | */ 10 | export default class Complex { 11 | re: number 12 | im: number 13 | 14 | /** 15 | * Creates a complex number 16 | * 17 | * @param re - The first input number 18 | * @param im - The second input number 19 | * @returns Creates a complex number `z = z.re + i z.im ` 20 | */ 21 | constructor(re: number, im = 0) { 22 | this.re = re 23 | this.im = im 24 | } 25 | 26 | /** 27 | * Radius in polar coordinate 28 | * @returns number 29 | */ 30 | get r(): number { 31 | return this.abs() 32 | } 33 | 34 | /** 35 | * Phi angle in polar coordinate 36 | * @returns angle 37 | */ 38 | get phi(): number { 39 | return this.arg() 40 | } 41 | 42 | /** 43 | * Phi angle in polar coordinate with TAU 44 | * @returns angle divided by TAU 45 | */ 46 | get phiTau(): number { 47 | return this.arg() / TAU 48 | } 49 | 50 | /** 51 | * Length squared: intensity probability 52 | * @returns number 53 | */ 54 | abs2(): number { 55 | return this.re * this.re + this.im * this.im 56 | } 57 | 58 | /** 59 | * Absolute value (length) 60 | * @returns absolute value 61 | */ 62 | abs(): number { 63 | return Math.sqrt(this.re * this.re + this.im * this.im) 64 | } 65 | 66 | /** 67 | * Complex number argument in range [0,Tau] 68 | * @returns number 69 | */ 70 | arg(): number { 71 | let arg = Math.atan2(this.im, this.re) 72 | if (arg < 0) { 73 | arg += TAU 74 | } 75 | return arg 76 | } 77 | 78 | /** 79 | * Complex conjugation 80 | * @returns z = z{re, -im} 81 | */ 82 | conj(): Complex { 83 | return new Complex(this.re, -this.im) 84 | } 85 | 86 | /** 87 | * Addition 88 | * @param z2 complex number to be added 89 | * @returns z = z1 + z2 90 | */ 91 | add(z2: Complex): Complex { 92 | const z1 = this 93 | return new Complex(z1.re + z2.re, z1.im + z2.im) 94 | } 95 | 96 | /** 97 | * Substraction 98 | * @param z2 complex number to be added 99 | * @returns z = z1 - z2 100 | */ 101 | sub(z2: Complex): Complex { 102 | const z1 = this 103 | return new Complex(z1.re - z2.re, z1.im - z2.im) 104 | } 105 | 106 | /** 107 | * Multiplication 108 | * @param z2 complex number to be multiplied 109 | * @returns z = z1 * z2 110 | */ 111 | mul(z2: Complex): Complex { 112 | const z1 = this 113 | return new Complex(z1.re * z2.re - z1.im * z2.im, z1.re * z2.im + z1.im * z2.re) 114 | } 115 | 116 | /** 117 | * Gauss complex multiplication algorithm 118 | * https://en.wikipedia.org/wiki/Multiplication_algorithm#Complex_multiplication_algorithm 119 | * @param z2 complex number to be multiplied 120 | * @returns z = z1 * z2 121 | */ 122 | mulGauss(z2: Complex): Complex { 123 | const z1 = this 124 | const k1 = z2.re * (z1.re + z1.im) 125 | const k2 = z1.re * (z2.im - z2.re) 126 | const k3 = z1.im * (z2.re + z2.im) 127 | return new Complex(k1 - k3, k1 + k2) 128 | } 129 | 130 | /** 131 | * Division 132 | * @param z2 complex number denominator 133 | * @returns z = z1 / z2 134 | */ 135 | div(z2: Complex): Complex { 136 | const z1 = this 137 | const denom = z2.im * z2.im + z2.re * z2.re 138 | if (denom === 0) { 139 | throw new Error(`Cannot divide by 0. z1: ${this.toString()} / z2: ${z2.toString()}`) 140 | } 141 | const re = (z1.re * z2.re + z1.im * z2.im) / denom 142 | const im = (z2.re * z1.im - z1.re * z2.im) / denom 143 | return new Complex(re, im) 144 | } 145 | 146 | /* eslint-disable max-len */ 147 | /** 148 | * Normalize 149 | * https://www.khanacademy.org/computing/computer-programming/programming-natural-simulations/programming-vectors/a/vector-magnitude-normalization 150 | * @returns z 151 | */ 152 | normalize(): Complex { 153 | const norm = this.r 154 | if (norm !== 0) { 155 | return new Complex(this.re / norm, this.im / norm) 156 | } else { 157 | throw new Error('Cannot normalize a 0 length vector...') 158 | } 159 | } 160 | 161 | /** 162 | * Tests if a complex is close to another 163 | * @param z2 complex to test proximity 164 | * @param eps tolerance for the Euclidean norm 165 | * @returns z1 ~= z2 166 | */ 167 | isCloseTo(z2: Complex, eps = 1e-6): boolean { 168 | return this.sub(z2).r < eps 169 | } 170 | 171 | /** 172 | * Tests if a complex is equal to another 173 | * @param z2 complex to test equality 174 | * @returns z1 === z2 175 | */ 176 | equal(z2: Complex): boolean { 177 | return this.re === z2.re && this.im === z2.im 178 | } 179 | 180 | /** 181 | * Check if a complex number is zero 182 | * @return z1 === 0 183 | */ 184 | isZero(): boolean { 185 | return this.re === 0 && this.im === 0 186 | } 187 | 188 | /** 189 | * Check if a complex number is one 190 | * @return z1 === 1 191 | */ 192 | isOne(): boolean { 193 | return this.re === 1 && this.im === 0 194 | } 195 | 196 | /** 197 | * Check if a complex number is very close zero (norm at most 1e-6 from zero) 198 | * @return z1 ~= 0 199 | */ 200 | isAlmostZero(): boolean { 201 | return this.abs2() < 1e-12 202 | } 203 | 204 | /** 205 | * Check if a complex number is normalized 206 | * @return z1.r === 1 207 | */ 208 | isNormal(): boolean { 209 | return this.abs2() === 1 210 | } 211 | 212 | /** 213 | * Override toString() method 214 | * @param complexFormat choice between ["cartesian", "polar", "polarTau"] 215 | * @param precision float display precision 216 | * @returns string with appropriate format 217 | */ 218 | toString(complexFormat: ComplexFormat = 'cartesian', precision = 2): string { 219 | switch (complexFormat) { 220 | case 'cartesian': 221 | return `(${this.re.toFixed(precision)} ${this.im >= 0 ? '+' : ''}${this.im.toFixed(precision)}i)` 222 | case 'polar': 223 | return `${this.r.toFixed(precision)} exp(${this.phi.toFixed(precision)}i)` 224 | case 'polarTau': 225 | return `${this.r.toFixed(precision)} exp(${this.phiTau.toFixed(precision)}τi)` 226 | default: 227 | throw new Error(`complexFormat '${complexFormat}' is not in ['cartesian', 'polar', 'polarTau'].`) 228 | } 229 | } 230 | 231 | /** 232 | * Generate HSL color from complex number 233 | * See complex domain coloring 234 | * @returns RGB string 235 | */ 236 | toColor(): string { 237 | const angle = ((this.phi * 360) / TAU + 360) % 360 238 | return hslToHex(angle, 100, 100 - 50 * this.r) 239 | } 240 | 241 | /** 242 | * Create a complex number from polar coordinates 243 | * @param r Radius in polar coordinates 244 | * @param phi Angle in polar coordinates 245 | */ 246 | static fromPolar(r: number, phi: number): Complex { 247 | return new Complex(r * Math.cos(phi), r * Math.sin(phi)) 248 | } 249 | 250 | /** 251 | * Generates a random complex with Gaussian distribution 252 | * (with zero mean and unit variance) 253 | * using https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform. 254 | */ 255 | static randomGaussian(): Complex { 256 | const u = Math.random() 257 | const v = Math.random() 258 | return Complex.fromPolar(Math.sqrt(-2 * Math.log(u)), 2 * Math.PI * v) 259 | } 260 | } 261 | 262 | /** 263 | * Syntactic sugar for `new Complex(re, im)` 264 | * 265 | * @param re - The first input number 266 | * @param im - The second input number 267 | * @returns Creates a complex number `z = z.re + i * z.im ` 268 | */ 269 | export function Cx(re: number, im = 0): Complex { 270 | return new Complex(re, im) 271 | } 272 | -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | export const TAU = 2 * Math.PI 2 | export const DEG_TO_RAD = TAU / 360 3 | -------------------------------------------------------------------------------- /src/Dimension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line */ 2 | import _ from 'lodash' 3 | 4 | const identityMap = new Map() 5 | 6 | /** 7 | * Dimension class, e.g. 8 | * - "polarization" with coordinates ["H", "V"] 9 | * - "direction" with coordinates [">", "^", "<", "v"] 10 | * Vide: http://nlp.seas.harvard.edu/NamedTensor and http://nlp.seas.harvard.edu/NamedTensor2 11 | */ 12 | export default class Dimension { 13 | /** A symbol used for Dimension comparison */ 14 | private readonly identity: symbol 15 | name: string 16 | size: number 17 | coordNames: string[] 18 | 19 | /** 20 | * Creates a dimentions - to be used in a Vector or Operator object 21 | * @param name Name of a dimension (e.g. "qubit", "spin", "x", etc) 22 | * @param size Size of a dimension 23 | * @param coordNames Name of each coordinate 24 | */ 25 | constructor(name: string, size: number, coordNames: string[]) { 26 | if (size !== coordNames.length) { 27 | throw new Error(`Coordinates [${coordNames}] array is of length ${coordNames.length}, not ${size}.`) 28 | } 29 | this.name = name 30 | this.coordNames = coordNames // later, we may make it optional 31 | this.size = size 32 | 33 | const hashKey = this.hashKey() 34 | let identity = identityMap.get(hashKey) 35 | if (identity == null) { 36 | identity = Symbol(name) 37 | identityMap.set(hashKey, identity) 38 | } 39 | this.identity = identity 40 | } 41 | 42 | /** 43 | * A string used for dimension equality test. 44 | * 45 | * @remark do not assume any particular format. The only important quality of this 46 | * value is that it fully encodes all relevant dimension data and can be used as a hash key 47 | * 48 | * @returns identification string 49 | */ 50 | private hashKey(): string { 51 | return `${this.name}-${this.size}-${this.coordNames.length}-${this.coordNames 52 | .map((n, i) => `#${i}-${n}`) 53 | .join('-')}` 54 | } 55 | 56 | /** 57 | * Create dimension for polarization 58 | * @param basis denote basis, ['H', 'V'], ['D', 'A'] or ['L', 'R'] 59 | * @returns polarization dimension 60 | */ 61 | static polarization(basis = ['H', 'V']): Dimension { 62 | return new Dimension('polarization', 2, basis) 63 | } 64 | 65 | /** 66 | * Create dimension for direction 67 | * @returns direction dimensions 68 | */ 69 | static direction(): Dimension { 70 | return new Dimension('direction', 4, ['>', '^', '<', 'v']) 71 | } 72 | 73 | /** 74 | * Create dimension for spin 75 | * @param basis denote basis, ['u', 'd'], ['ux', 'dx'] or ['dy', 'dy'] 76 | * @returns spin dimensions 77 | */ 78 | static spin(basis = ['u', 'd']): Dimension { 79 | return new Dimension('spin', 2, basis) 80 | } 81 | 82 | /** 83 | * Creates a dimension with positions from 0 to size-1 84 | * @param size A positive integer - size of steps. 85 | * @param name Dimension name, e.g. 'x', 'y' or 'z'. 86 | */ 87 | static position(size: number, name = 'x'): Dimension { 88 | const coordNames = _.range(size).map((i: number) => i.toString()) 89 | return new Dimension(name, size, coordNames) 90 | } 91 | 92 | /** 93 | * Create dimension for qubit (an abstract two-level system) 94 | * @note Equivalent to {@link Dimension.position}(2, 'qubit') 95 | * @param basis denote basis, ['0', '1'], ['+', '-'] or ['+i', '-i'] 96 | * @returns qubit dimension 97 | */ 98 | static qubit(basis = ['0', '1']): Dimension { 99 | return new Dimension('qubit', 2, basis) 100 | } 101 | 102 | /** 103 | * Create dimensions for n qubits. 104 | * @param n Number of qubits. 105 | * @returns Qubit dimensions. 106 | */ 107 | static qubits(n: number): Dimension[] { 108 | return _.range(n).map(() => Dimension.qubit()) 109 | } 110 | 111 | /** 112 | * Creates a copy with different coord names. 113 | * Used in operators that change basis. 114 | * @param coordNames 115 | */ 116 | reassignCoordNames(coordNames: string[]): Dimension { 117 | return new Dimension(this.name, this.size, coordNames) 118 | } 119 | 120 | /** 121 | * Overrides toString() method 122 | * @returns formatted string 123 | */ 124 | toString(): string { 125 | return `#Dimension [${this.name}] of size [${this.size.toString()}] has coordinates named: [${this.coordNames}]` 126 | } 127 | 128 | /** 129 | * @returns string with concat names 130 | */ 131 | get coordString(): string { 132 | return this.coordNames.join('') 133 | } 134 | 135 | /** 136 | * Test equality between two dimensions 137 | * @param dim2 138 | * @returns dim1 === dim 2 139 | */ 140 | isEqual(dim2: Dimension): boolean { 141 | return this.identity === dim2.identity 142 | } 143 | 144 | /** 145 | * Retrieves a coordinates index from the coordinates list 146 | * @param coordName one coord bame 147 | * @returns error or the coord index 148 | */ 149 | coordNameToIndex(coordName: string): number { 150 | const idx = this.coordNames.indexOf(coordName) 151 | if (idx < 0) { 152 | throw new Error(`${coordName} is not in [${this.coordNames}]`) 153 | } else { 154 | return idx 155 | } 156 | } 157 | 158 | /** 159 | * Concat the names od dimensions 160 | * @param dims Array of dimensions 161 | */ 162 | static concatDimNames(dims: Dimension[]): string { 163 | let names = '' 164 | dims.forEach((dim) => { 165 | names += dim.coordString 166 | }) 167 | return names 168 | } 169 | 170 | /** 171 | * Check multiple dimensions for equality 172 | * Needs rewrite imo 173 | * @param dims1 First dimension instance 174 | * @param dims2 second dimension instance 175 | */ 176 | static checkDimensions(dims1: Dimension[], dims2: Dimension[]): void { 177 | // Check for size 178 | if (dims1.length !== dims2.length) { 179 | console.error( 180 | `Dimensions with unequal number of components ${dims1.length} !== ${dims2.length}.\n 181 | Dimensions 1:\n${dims1.join('\n')}\n 182 | Dimensions 2:\n${dims2.join('\n')}`, 183 | ) 184 | throw new Error('Dimensions array size mismatch...') 185 | } 186 | // Check for order 187 | for (let i = 0; i < dims1.length; i++) { 188 | if (!dims1[i].isEqual(dims2[i])) { 189 | console.error( 190 | `Dimensions have the same number of components, but the component ${i} is\n${dims1[i]}\nvs\n${dims2[i]}.\n 191 | Dimensions 1:\n${dims1.join('\n')}\n 192 | Dimensions 2:\n${dims2.join('\n')}`, 193 | ) 194 | throw new Error('Dimensions array order mismatch...') 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * 201 | * @param s string, such as "udH" or ['u', 'd', 'H'] 202 | * @param dimensions Dimensions to be used 203 | * 204 | * @returns 205 | */ 206 | static stringToCoordIndices(s: string | string[], dimensions: Dimension[]): number[] { 207 | if (dimensions.length !== s.length) { 208 | throw `dimensions.length (${dimensions.length}) !== string.length (${s.length})` 209 | } 210 | return _.range(dimensions.length).map((i) => dimensions[i].coordNameToIndex(s[i])) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Elements.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line */ 2 | import _ from 'lodash' 3 | import { Cx } from './Complex' 4 | import Dimension from './Dimension' 5 | import Vector from './Vector' 6 | import Operator from './Operator' 7 | import { DEG_TO_RAD, TAU } from './Constants' 8 | import * as ops from './Ops' 9 | import { Elem, ICell, IXYOperator } from './interfaces' 10 | 11 | const dimPol = Dimension.polarization() 12 | const dimDir = Dimension.direction() 13 | const idPol = Operator.identity([dimPol]) 14 | const idDir = Operator.identity([dimDir]) 15 | const projH = Operator.indicator([dimPol], 'H') 16 | const projV = Operator.indicator([dimPol], 'V') 17 | 18 | const mod = (x: number, n: number): number => ((x % n) + n) % n 19 | 20 | /** 21 | * Sugar solution (for polarization rotation) 22 | * @param rot Rotation angle (in TAU) for polarization. By default 1/8 (45deg). 23 | * For more sugar cubes, 2/8 and 3/8 make sense. 24 | */ 25 | export function sugarSolution(polarizationRotation = 0.125): Operator { 26 | return Operator.outer([idDir, ops.rotationMatrix(polarizationRotation * TAU, dimPol)]) 27 | } 28 | 29 | /** 30 | * An attenuator, or: neutral density filter 31 | * @param r Amplitude attenuation factor. Intensity is changed by r^2. 32 | * By default it absorbs 50% photons. 33 | */ 34 | export function attenuator(r = Math.SQRT1_2): Operator { 35 | return ops.amplitudeIntensity(r, 0) 36 | } 37 | 38 | /** 39 | * A vacuum jar - advances phase by lambda/4. 40 | */ 41 | export function vacuumJar(): Operator { 42 | return ops.amplitudeIntensity(1, -0.25) 43 | } 44 | 45 | /** 46 | * A glass slab - delays phase by lambda/4. 47 | */ 48 | export function glassSlab(): Operator { 49 | return ops.amplitudeIntensity(1, +0.25) 50 | } 51 | 52 | /** 53 | * A both-sided mirror, from metal or any other optically denser medium. 54 | * 0: -, 45: /, 90: |, 135: \ 55 | * @angle Angle in degrees, from -> CCW. Needs to be multiple of 45deg. 56 | * @returns Operator with dimensions [dimDir, dimPol]. 57 | */ 58 | export function mirror(angle: number): Operator { 59 | return Operator.outer([ops.reflectFromPlaneDirection(angle), ops.reflectPhaseFromDenser()]) 60 | } 61 | 62 | /** 63 | * A symmetric non-polarizing beam splitter. 64 | * Think: a very thin slab of glass. 65 | * 0: -, 45: /, 90: |, 135: \ 66 | * @angle Angle in degrees, from -> CCW. Needs to be multiple of 45deg. 67 | * @returns Operator with dimensions [dimDir, dimPol]. 68 | * @todo CHECK reflection phase. 69 | */ 70 | export function beamSplitter(angle: number, split = 0.5): Operator { 71 | const reflect = ops 72 | .reflectFromPlaneDirection(angle) 73 | .outer(ops.reflectPhaseFromDenser()) 74 | .mulConstant(Cx(0.0, Math.sqrt(split))) 75 | const transmit = ops 76 | .beamsplitterTransmittionDirections(angle) 77 | .outer(idPol) 78 | .mulConstant(Cx(Math.sqrt(1 - split), 0.0)) 79 | return reflect.add(transmit) 80 | } 81 | 82 | /** 83 | * A corner cube, from in all 4 directions. 84 | * https://en.wikipedia.org/wiki/Corner_reflector 85 | */ 86 | export function cornerCube(): Operator { 87 | return Operator.outer([ 88 | Operator.fromSparseCoordNames( 89 | [ 90 | ['<', '>', Cx(1)], 91 | ['v', '^', Cx(1)], 92 | ['>', '<', Cx(1)], 93 | ['^', 'v', Cx(1)], 94 | ], 95 | [dimDir], 96 | ), 97 | idPol, 98 | ]) 99 | } 100 | 101 | /** 102 | * A polarizing beam splitter. 103 | * Think: a very thin slab of glass. 104 | * 0: [/], 90: [\] 105 | * @note Changed convention from 45 and 135deg! 106 | * @angle Angle in degrees, from -> CCW. Needs to be 0 or 90, up to 180deg. 107 | * @returns Operator with dimensions [dimDir, dimPol]. 108 | */ 109 | export function polarizingBeamsplitter(angle: number): Operator { 110 | if (!_.includes([0, 90], mod(angle, 180))) { 111 | throw new Error(`polarizingBeamsplitter: angle ${angle} mod 180 not in [0, 90].`) 112 | } 113 | 114 | return Operator.add([idDir.outer(projH), ops.reflectFromPlaneDirection(angle + 45).outer(projV)]) 115 | } 116 | 117 | /** 118 | * Faraday rotator (for polarization rotation) 119 | * https://en.wikipedia.org/wiki/Faraday_rotator 120 | * @angle Angle in degrees, from -> CCW. Needs to be multiple of 90. 121 | * 0: ->, 45: ^, 90: <-, 135: v 122 | * @param rot Rotation angle (in TAU) for polarization. By default 1/8 (45deg). 123 | * For more current, 2/8 and 3/8 make sense. 124 | */ 125 | export function faradayRotator(angle: number, polarizationRotation = 0.125): Operator { 126 | return Operator.add([ 127 | Operator.outer([ops.diodeForDirections(angle), ops.rotationMatrix(polarizationRotation * TAU, dimPol)]), 128 | Operator.outer([ops.diodeForDirections(angle + 180), ops.rotationMatrix(-polarizationRotation * TAU, dimPol)]), 129 | ]) 130 | } 131 | 132 | /** 133 | * A linear polarizer. 134 | * @param angle In plane rotation, in degrees [0, 90, 180, 270], i.e | - | -. 135 | * @param polarizationOrientation A number, in tau, i.e. [0, 1]. 0 transmits hotizontal polarization, 0.25 - vertical. 136 | * @todo Check angle conventions. 137 | */ 138 | export function polarizer(angle: number): Operator { 139 | return Operator.add([ 140 | Operator.outer([ops.diodeForDirections(0), ops.projectionMatrix(angle * DEG_TO_RAD, dimPol)]), 141 | Operator.outer([ops.diodeForDirections(90), ops.projectionMatrix((angle + 90) * DEG_TO_RAD, dimPol)]), 142 | Operator.outer([ops.diodeForDirections(180), ops.projectionMatrix((angle + 180) * DEG_TO_RAD, dimPol)]), 143 | Operator.outer([ops.diodeForDirections(270), ops.projectionMatrix((angle + 270) * DEG_TO_RAD, dimPol)]), 144 | ]) 145 | } 146 | 147 | /** 148 | * A phase plate for linear polarization. 149 | * @param rotation Element rotation in degrees 150 | * @param phase Phase shift in TAU. 1/4 for quater-wave-plate, 1/2 for half-wave-plate. 151 | * @todo Convention: modify this polarization, ortonogal, or some other way? 152 | */ 153 | export function phasePlate(rotation: number, phaseShift: number): Operator { 154 | return Operator.add([ 155 | Operator.outer([ 156 | ops.diodeForDirections(0), 157 | ops.phaseShiftForRealEigenvectors(rotation * DEG_TO_RAD, 0, phaseShift, dimPol), 158 | ]), 159 | Operator.outer([ 160 | ops.diodeForDirections(90), 161 | ops.phaseShiftForRealEigenvectors((rotation + 90) * DEG_TO_RAD, 0, phaseShift, dimPol), 162 | ]), 163 | Operator.outer([ 164 | ops.diodeForDirections(180), 165 | ops.phaseShiftForRealEigenvectors((rotation + 180) * DEG_TO_RAD, 0, phaseShift, dimPol), 166 | ]), 167 | Operator.outer([ 168 | ops.diodeForDirections(270), 169 | ops.phaseShiftForRealEigenvectors((rotation + 270) * DEG_TO_RAD, 0, phaseShift, dimPol), 170 | ]), 171 | ]) 172 | } 173 | 174 | /** 175 | * Turn operator from coherent, polarized light, to incoherent, non-polarized intensity. 176 | * @param opDirPol Operator with [direction,polarization] dimensions. 177 | * @return Operator with real values and dimenson [direction]. 178 | */ 179 | export function incoherentLightOperator(opDirPol: Operator): Operator { 180 | const opIntensity = opDirPol.mapValues((z) => z.mul(z.conj())) 181 | const polInputs = Vector.fromArray([Cx(0.5), Cx(0.5)], [Dimension.polarization()]) 182 | const polOutpus = Vector.fromArray([Cx(1), Cx(1)], [Dimension.polarization()]) 183 | return opIntensity.contractLeft([1], polOutpus).contractRight([1], polInputs) 184 | } 185 | 186 | /** 187 | * Compute local operator for given cell 188 | */ 189 | function cellLocalOperator(cell: ICell): Operator { 190 | switch (cell.element) { 191 | case Elem.Absorber: 192 | return attenuator(Math.sqrt(cell.strength ?? 0.5)) 193 | case Elem.BeamSplitter: 194 | return beamSplitter(cell.rotation, cell.split ?? 0.5) 195 | case Elem.CoatedBeamSplitter: 196 | return beamSplitter(cell.rotation, cell.split ?? 0.5) 197 | case Elem.CornerCube: 198 | return cornerCube() 199 | case Elem.Detector: 200 | return attenuator(0) 201 | case Elem.DetectorFour: 202 | return attenuator(0) 203 | case Elem.FaradayRotator: 204 | return faradayRotator(cell.rotation) 205 | case Elem.Gate: 206 | return attenuator(0) 207 | case Elem.Glass: 208 | return glassSlab() 209 | case Elem.Laser: 210 | return attenuator(0) 211 | case Elem.Mine: 212 | return attenuator(0) 213 | case Elem.Mirror: 214 | return mirror(cell.rotation) 215 | case Elem.NonLinearCrystal: 216 | return attenuator(1) 217 | case Elem.Polarizer: 218 | return polarizer(cell.rotation) 219 | case Elem.PolarizingBeamSplitter: 220 | return polarizingBeamsplitter(cell.rotation) 221 | case Elem.HalfWavePlate: 222 | return phasePlate(cell.rotation, 0.5) 223 | case Elem.QuarterWavePlate: 224 | return phasePlate(cell.rotation, 0.25) 225 | case Elem.Rock: 226 | return attenuator(0) 227 | case Elem.SugarSolution: 228 | return sugarSolution(0.25) 229 | case Elem.VacuumJar: 230 | return vacuumJar() 231 | case Elem.Void: 232 | return attenuator(1) 233 | case Elem.Wall: 234 | return attenuator(0) 235 | default: { 236 | throw new Error(`Conversion from cell to operator failed: ${cell}.`) 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Compute operators from the grid cells 243 | * @param cell, an ICell interface 244 | * @returns IXYOperator 245 | */ 246 | export function generateOperator(cell: ICell): IXYOperator { 247 | const x = cell.x 248 | const y = cell.y 249 | return { 250 | x, 251 | y, 252 | op: cellLocalOperator(cell), 253 | } 254 | } 255 | 256 | /** 257 | * Compute list of operators from the grid 258 | * @param grid 259 | * @returns IXYOperator[] list 260 | */ 261 | export function generateOperators(cells: ICell[]): IXYOperator[] { 262 | return cells.map((cell) => { 263 | return generateOperator(cell) 264 | }) 265 | } 266 | -------------------------------------------------------------------------------- /src/Entanglement.ts: -------------------------------------------------------------------------------- 1 | import Vector from './Vector' 2 | import Operator from './Operator' 3 | 4 | /** 5 | * Class for measuring entanglement. 6 | */ 7 | export default class Entanglement { 8 | /** 9 | * Renyi-2 entanglement entropy for subsystem split A-B. 10 | * @param v Normalized vector representing a pure state. 11 | * @param coordIndices Indices related to a subsystem (either A or B). 12 | * @returns - log_2 Tr[rho_A^2] 13 | * @see https://en.wikipedia.org/wiki/Entropy_of_entanglement 14 | * @note It can be optimized if we omit creating the full density matrix. 15 | */ 16 | static renyi2(v: Vector, coordIndices: number[]): number { 17 | const rhoAB = Operator.projectionOn(v) 18 | const rhoB = rhoAB.partialTrace(coordIndices) 19 | return -Math.log2(rhoB.mulOp(rhoB).trace().re) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Gates.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Quantum gates 3 | * https://en.wikipedia.org/wiki/Quantum_logic_gate 4 | */ 5 | 6 | import Complex, { Cx } from './Complex' 7 | import Dimension from './Dimension' 8 | import Operator from './Operator' 9 | 10 | const isqrt2 = Cx(Math.SQRT1_2) 11 | 12 | /** 13 | * Pauli I operator 14 | * @return one-qubit operator 15 | */ 16 | export function I(): Operator { 17 | return Operator.fromSparseCoordNames( 18 | [ 19 | ['0', '0', Cx(1)], 20 | ['1', '1', Cx(1)], 21 | ], 22 | [Dimension.qubit()], 23 | ) 24 | } 25 | 26 | /** 27 | * Pauli X operator 28 | * @see https://en.wikipedia.org/wiki/Pauli_matrices 29 | * @return one-qubit operator 30 | */ 31 | export function X(): Operator { 32 | return Operator.fromSparseCoordNames( 33 | [ 34 | ['0', '1', Cx(1)], 35 | ['1', '0', Cx(1)], 36 | ], 37 | [Dimension.qubit()], 38 | ) 39 | } 40 | 41 | /** 42 | * Pauli Y operator 43 | * @see https://en.wikipedia.org/wiki/Pauli_matrices 44 | * @return one-qubit operator 45 | */ 46 | export function Y(): Operator { 47 | return Operator.fromSparseCoordNames( 48 | [ 49 | ['0', '1', Cx(0, -1)], 50 | ['1', '0', Cx(0, 1)], 51 | ], 52 | [Dimension.qubit()], 53 | ) 54 | } 55 | 56 | /** 57 | * Pauli Z operator 58 | * @see https://en.wikipedia.org/wiki/Pauli_matrices 59 | * @return one-qubit operator 60 | */ 61 | export function Z(): Operator { 62 | return Operator.fromSparseCoordNames( 63 | [ 64 | ['0', '0', Cx(1)], 65 | ['1', '1', Cx(-1)], 66 | ], 67 | [Dimension.qubit()], 68 | ) 69 | } 70 | 71 | /** 72 | * Hadaamard gate 73 | * @see https://en.wikipedia.org/wiki/Hadamard_transform 74 | * @return one-qubit operator 75 | */ 76 | export function H(): Operator { 77 | return Operator.fromSparseCoordNames( 78 | [ 79 | ['0', '0', Cx(1)], 80 | ['1', '0', Cx(1)], 81 | ['0', '1', Cx(1)], 82 | ['1', '1', Cx(-1)], 83 | ], 84 | [Dimension.qubit()], 85 | ).mulConstant(isqrt2) 86 | } 87 | 88 | /** 89 | * Phase operator (shifts by pi/2 = 1/4 full rotation) 90 | * @return one-qubit operator 91 | */ 92 | export function S(): Operator { 93 | return Operator.fromSparseCoordNames( 94 | [ 95 | ['0', '0', Cx(1)], 96 | ['1', '1', Cx(0, 1)], 97 | ], 98 | [Dimension.qubit()], 99 | ) 100 | } 101 | 102 | /** 103 | * Phase operator (shifts by pi/4 = 1/8 full rotation) 104 | * @return one-qubit operator 105 | */ 106 | export function T(): Operator { 107 | return Operator.fromSparseCoordNames( 108 | [ 109 | ['0', '0', Cx(1)], 110 | ['1', '1', Complex.fromPolar(1, Math.PI / 4)], 111 | ], 112 | [Dimension.qubit()], 113 | ) 114 | } 115 | 116 | /** 117 | * Controlled not gate (CNOT) 118 | * @return two-qubit operator 119 | */ 120 | export function CX(): Operator { 121 | return Operator.fromSparseCoordNames( 122 | [ 123 | ['00', '00', Cx(1)], 124 | ['01', '01', Cx(1)], 125 | ['10', '11', Cx(1)], 126 | ['11', '10', Cx(1)], 127 | ], 128 | [Dimension.qubit(), Dimension.qubit()], 129 | ) 130 | } 131 | 132 | /** 133 | * Controlled Z gate 134 | * @return two-qubit operator 135 | */ 136 | export function CZ(): Operator { 137 | return Operator.fromSparseCoordNames( 138 | [ 139 | ['00', '00', Cx(1)], 140 | ['01', '01', Cx(1)], 141 | ['10', '10', Cx(1)], 142 | ['11', '11', Cx(-1)], 143 | ], 144 | [Dimension.qubit(), Dimension.qubit()], 145 | ) 146 | } 147 | 148 | /** 149 | * Two-quibit swap operator 150 | * @return two-qubit operator 151 | */ 152 | export function Swap(): Operator { 153 | return Operator.fromSparseCoordNames( 154 | [ 155 | ['00', '00', Cx(1)], 156 | ['10', '01', Cx(1)], 157 | ['01', '10', Cx(1)], 158 | ['11', '11', Cx(1)], 159 | ], 160 | [Dimension.qubit(), Dimension.qubit()], 161 | ) 162 | } 163 | 164 | /** 165 | * Toffoli gate (CCNOT) 166 | * @see https://en.wikipedia.org/wiki/Toffoli_gate 167 | * @return two-qubit operator 168 | */ 169 | export function CCX(): Operator { 170 | return Operator.fromSparseCoordNames( 171 | [ 172 | ['000', '000', Cx(1)], 173 | ['001', '001', Cx(1)], 174 | ['010', '010', Cx(1)], 175 | ['011', '011', Cx(1)], 176 | ['100', '100', Cx(1)], 177 | ['101', '101', Cx(1)], 178 | ['111', '110', Cx(1)], // this and 179 | ['110', '111', Cx(1)], // that differs from identity 180 | ], 181 | [Dimension.qubit(), Dimension.qubit(), Dimension.qubit()], 182 | ) 183 | } 184 | 185 | /** 186 | * Fredkin gate (CCNOT) 187 | * @see https://en.wikipedia.org/wiki/Fredkin_gate 188 | * @return two-qubit operator 189 | */ 190 | export function CSwap(): Operator { 191 | return Operator.fromSparseCoordNames( 192 | [ 193 | ['000', '000', Cx(1)], 194 | ['001', '001', Cx(1)], 195 | ['010', '010', Cx(1)], 196 | ['011', '011', Cx(1)], 197 | ['100', '100', Cx(1)], 198 | ['110', '101', Cx(1)], // this and 199 | ['101', '110', Cx(1)], // that differs from identity 200 | ['111', '111', Cx(1)], 201 | ], 202 | [Dimension.qubit(), Dimension.qubit(), Dimension.qubit()], 203 | ) 204 | } 205 | -------------------------------------------------------------------------------- /src/IncoherentLight.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line */ 2 | import _ from 'lodash' 3 | import { IXYOperator } from './interfaces' 4 | import Vector from './Vector' 5 | import Operator from './Operator' 6 | import Dimension from './Dimension' 7 | import Photons from './Photons' 8 | import { incoherentLightOperator } from './Elements' 9 | import { Cx } from './Complex' 10 | 11 | /** 12 | * For classic, incoherent light: 13 | * only intensity, not phase or polarization 14 | */ 15 | export default class IncoherentLight { 16 | vector: Vector 17 | 18 | /** 19 | * Create a board for incoherent light. 20 | * Mostly for internal use. 21 | * @param sizeX An integer, size x (width) of the board. 22 | * @param sizeY An integer, size y (height) of the board. 23 | * @param vector Vector with [x1 y, dir]. 24 | */ 25 | constructor(vector: Vector) { 26 | this.vector = vector 27 | } 28 | 29 | /** 30 | * Create an empty board for photons, with a given size. 31 | * @param sizeX An integer, size x (width) of the board. 32 | * @param sizeY An integer, size y (height) of the board. 33 | */ 34 | static emptySpace(sizeX: number, sizeY: number): IncoherentLight { 35 | const vector = Vector.zeros([Dimension.position(sizeX, 'x'), Dimension.position(sizeY, 'y'), Dimension.direction()]) 36 | return new IncoherentLight(vector) 37 | } 38 | 39 | /** 40 | * Dimension x. 41 | */ 42 | get dimX(): Dimension { 43 | return this.vector.dimensions[0] 44 | } 45 | 46 | /** 47 | * Dimension x. 48 | */ 49 | get dimY(): Dimension { 50 | return this.vector.dimensions[1] 51 | } 52 | 53 | /** 54 | * Size x ('width') of the board. 55 | */ 56 | get sizeX(): number { 57 | return this.dimX.size 58 | } 59 | 60 | /** 61 | * Size y ('height') of the board. 62 | */ 63 | get sizeY(): number { 64 | return this.dimY.size 65 | } 66 | 67 | /** 68 | * @returns A deep copy of the same object. 69 | */ 70 | copy(): IncoherentLight { 71 | return new IncoherentLight(this.vector.copy()) 72 | } 73 | 74 | /** 75 | * Total intensity 76 | */ 77 | get totalIntensity(): number { 78 | return this.vector.entries.map((entry) => entry.value).reduce((a, b) => a.add(b), Cx(0)).re 79 | } 80 | 81 | /** 82 | * Normalize. Unlike in photos, we normalize by sum. 83 | * @returns Itself, for chaining. 84 | */ 85 | normalize(): IncoherentLight { 86 | this.vector = this.vector.mulByReal(1 / this.totalIntensity) 87 | return this 88 | } 89 | 90 | /** 91 | * Create a single photon vector. 92 | * @param sizeX Board size, x. 93 | * @param sizeY Board size, y. 94 | * @param posX Position of the photon, x. 95 | * @param posY Position of the photon, y. 96 | * @param dirDirection Direction from ['>', '^', '<', 'v]. 97 | * 98 | * @returns A vector [dimX, DimY, dir, pol], does not modify the object. 99 | */ 100 | static vectorFromIndicator(sizeX: number, sizeY: number, posX: number, posY: number, dir: string): Vector { 101 | const dimensions = [Dimension.position(sizeX, 'x'), Dimension.position(sizeY, 'y'), Dimension.direction()] 102 | const state = [posX.toString(), posY.toString(), dir] 103 | 104 | return Vector.indicator(dimensions, state) 105 | } 106 | 107 | /** 108 | * Add one more photon to the state, using {@link Photons.vectorFromIndicator}. 109 | * 110 | * @param posX Position of the photon, x. 111 | * @param posY Position of the photon, y. 112 | * @param dir Direction from ['>', '^', '<', 'v]. 113 | * 114 | * @returns Itself, for chaining. 115 | */ 116 | addIntensityFromIndicator(posX: number, posY: number, dir: string, intensity = 1): IncoherentLight { 117 | const newIntensity = IncoherentLight.vectorFromIndicator(this.sizeX, this.sizeY, posX, posY, dir).mulConstant( 118 | Cx(intensity), 119 | ) 120 | this.vector = this.vector.add(newIntensity) 121 | return this 122 | } 123 | 124 | /** 125 | * Create a propagator, given the board size. Same as for {@link Photons.propagator} 126 | * @param sizeX Board size, x. 127 | * @param sizeY Board size, y. 128 | * @param yDirMeansDown For true, direction 'v' increments dimY. 129 | * 130 | * @return An operator, with dimensions [dimX, dimY, {@link Dimension.direction()}]. 131 | */ 132 | static propagator(sizeX: number, sizeY: number, yDirMeansDown = true): Operator { 133 | return Photons.propagator(sizeX, sizeY, yDirMeansDown) 134 | } 135 | 136 | /** 137 | * Propagate all particles, using {@link createPhotonPropagator}. 138 | * @param yDirMeansDown or true, direction 'v' increments dimY. 139 | * 140 | * @returns Itself, for chaining. 141 | */ 142 | propagateBeam(yDirMeansDown = true): IncoherentLight { 143 | const photonPropagator = Photons.propagator(this.sizeX, this.sizeY, yDirMeansDown) 144 | this.vector = photonPropagator.mulVec(this.vector) 145 | return this 146 | } 147 | 148 | /** 149 | * Create an operator for a particular place, projecting only on the particular position. 150 | * @param sizeX Board size, x. 151 | * @param sizeY Board size, y. 152 | * @param posX Position x. 153 | * @param posY Posiiton y. 154 | * @param op Operator, assumed to be with dimensions [dir]. 155 | * 156 | * @returns An operator [dimX, dimY, pol, dir]. 157 | */ 158 | static localizeOperator(sizeX: number, sizeY: number, posX: number, posY: number, op: Operator): Operator { 159 | const dimX = Dimension.position(sizeX, 'x') 160 | const dimY = Dimension.position(sizeY, 'y') 161 | return Operator.outer([Operator.indicator([dimX, dimY], [`${posX}`, `${posY}`]), op]) 162 | } 163 | 164 | /** 165 | * Turn an list of operators in a complete one-photon iteraction operator for the board. 166 | * @param sizeX Board size, x. 167 | * @param sizeY Board size, y. 168 | * @param opsWithPos A list of [x, y, operator with [dir]]. 169 | */ 170 | static interactionOperator(sizeX: number, sizeY: number, opsWithPos: IXYOperator[]): Operator { 171 | const localizedOpsShifted = opsWithPos.map((d: IXYOperator) => { 172 | const { x, y, op } = d 173 | const idDirPol = Operator.identity([Dimension.direction()]) 174 | const shiftedOp = op.sub(idDirPol) 175 | return Photons.localizeOperator(sizeX, sizeY, { x, y, op: shiftedOp }) 176 | }) 177 | 178 | const dimX = Dimension.position(sizeX, 'x') 179 | const dimY = Dimension.position(sizeY, 'y') 180 | 181 | return Operator.add([Operator.identity([dimX, dimY, Dimension.direction()]), ...localizedOpsShifted]) 182 | } 183 | 184 | /** 185 | * Act on single photons with a given set of operations. 186 | * @remark Absorption for states with n>1 photons is broken. 187 | * - it tracks only a fixed-number of photons subspace. 188 | * @param opsWithPos A list of [x, y, operator with [dir, pol]]. 189 | * 190 | * @returns Itself, for chaining. 191 | */ 192 | interact(opsWithPos: IXYOperator[]): IncoherentLight { 193 | const interactionOperator = IncoherentLight.interactionOperator(this.sizeX, this.sizeY, opsWithPos) 194 | this.vector = interactionOperator.mulVec(this.vector) 195 | return this 196 | } 197 | 198 | static opsWithPosMakeIncoherent(opsWithPos: IXYOperator[]): IXYOperator[] { 199 | return opsWithPos.map(({ x, y, op }) => ({ 200 | x, 201 | y, 202 | op: incoherentLightOperator(op), 203 | })) 204 | } 205 | 206 | /** 207 | * Generates a string for kets. 208 | * As there are only real number, I remove others. 209 | * @param precision Float precision. 210 | * 211 | * @returns A ket string, e.g. 0.75 |3,1,>⟩ + 0.25 |2,2,v⟩. 212 | */ 213 | ketString(precision = 2): string { 214 | return this.vector 215 | .toString('cartesian', precision, ' + ', false) 216 | .replace(/\(/g, '') 217 | .replace(/ \+0\.0*i\)/g, '') 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Measurement.ts: -------------------------------------------------------------------------------- 1 | import Vector from './Vector' 2 | import Operator from './Operator' 3 | 4 | export interface INamedVector { 5 | name: string[] 6 | vector: Vector 7 | } 8 | 9 | /** 10 | * Class for creating and using weighted projections M=wP for POVMs. 11 | */ 12 | export class WeightedProjection { 13 | name: string[] 14 | weight: number 15 | operator: Operator 16 | 17 | constructor(name: string[], operator: Operator, weight: number) { 18 | this.name = name 19 | this.operator = operator 20 | this.weight = weight 21 | } 22 | 23 | /** 24 | * Created a weighted projection 25 | * @param name Name (it will be displayed in measurement results) 26 | * @param operator A projective operator (P^2=P) 27 | * @param weight Weight w, so that M=wP 28 | * @param check Check if P is indeed a projection 29 | * @returns 30 | */ 31 | static new(name: string[], operator: Operator, weight = 1, check = true): WeightedProjection { 32 | if (check && !operator.isCloseToProjection()) { 33 | throw Error(`WeightedProjection ${name.join('&')} is not a projection.`) 34 | } 35 | return new WeightedProjection(name, operator, weight) 36 | } 37 | 38 | /** 39 | * Turn a vector (|v|^2 = probability) into an measurement operator. 40 | * M = |v> -> √w P |psi> 63 | * @param vector Vector to act on 64 | * @param coordIndices Indices to act on 65 | * @returns 66 | */ 67 | actOnPureState(vector: Vector, coordIndices: number[]): Vector { 68 | return this.operator.mulVecPartial(coordIndices, vector).mulByReal(Math.sqrt(this.weight)) 69 | } 70 | 71 | /** 72 | * |psi> -> √w P |psi> and append measurement to named vector name 73 | * @param namedVector Named vector to act on 74 | * @param coordIndices Indices to act on 75 | * @returns 76 | */ 77 | actOnNamedVector(namedVector: INamedVector, coordIndices: number[]): INamedVector { 78 | return { 79 | name: [...namedVector.name, ...this.name], 80 | vector: this.actOnPureState(namedVector.vector, coordIndices), 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Class for performing measurements. 87 | * Results are labeled by names of types T. 88 | * Supports successive measurements, to deal with many particles. 89 | */ 90 | export default class Measurement { 91 | states: INamedVector[] 92 | 93 | constructor(states: INamedVector[]) { 94 | this.states = states 95 | } 96 | 97 | /** 98 | * Create measurement from a single state. Normalizes it. 99 | * @param vector Vector to start with. 100 | * @param name An optional vector name. 101 | */ 102 | static fromVector(vector: Vector, name: string[] = []): Measurement { 103 | return new Measurement([{ name, vector: vector.normalize() }]) 104 | } 105 | 106 | destructiveMeasurement(coordIndices: number[], projections: INamedVector[]): Measurement { 107 | const newStates = this.states.flatMap((state) => 108 | projections.map((projection) => ({ 109 | name: [...state.name, ...projection.name], 110 | vector: projection.vector.innerPartial(coordIndices, state.vector), 111 | })), 112 | ) 113 | return new Measurement(newStates) 114 | } 115 | 116 | nondemolitionMeasurement(coordIndices: number[], povms: WeightedProjection[], check = true): Measurement { 117 | if (check && !Operator.add(povms.map((povm) => povm.scaledOperator())).isCloseToIdentity()) { 118 | throw Error('POVMs do not add up to identity, fix or use projectiveMeasurement instead.') 119 | } 120 | const newStates = this.states.flatMap((state) => povms.map((povm) => povm.actOnNamedVector(state, coordIndices))) 121 | return new Measurement(newStates) 122 | } 123 | 124 | /** 125 | * A projective, destructive measurement. 126 | * @param coordIndices Coordinates to be measured. 127 | * @param measurements An array of vector projections. 128 | * @param povms An array of positive operators. 129 | * To make √M managable, they need to be be weighted arbitrary-dim projection operators. 130 | * @returns An array of projected states. Their norm squared is the probability. 131 | * @todo Separate this measurement (with "other" but requiring orthogonality) to two. 132 | */ 133 | projectiveMeasurement( 134 | coordIndices: number[], 135 | projections: INamedVector[], 136 | povms: WeightedProjection[] = [], 137 | ): Measurement { 138 | const newStatesProj = this.destructiveMeasurement(coordIndices, projections).states 139 | const newStatesPOVM = this.nondemolitionMeasurement(coordIndices, povms, false).states 140 | // |psi> -> |psi> + Σi(( √(1-w_i) - 1) P_i) |psi> 141 | const projOnMeasured = Operator.add( 142 | projections 143 | .map((projection) => WeightedProjection.fromVector([], projection.vector).remainingShift()) 144 | .concat(povms.map((povm) => povm.remainingShift())), 145 | ) 146 | const notMeasured = this.states.map(({ name, vector }) => { 147 | const projectedVec = vector.add(projOnMeasured.mulVecPartial(coordIndices, vector)) 148 | return { 149 | name, 150 | vector: projectedVec, 151 | } 152 | }) 153 | 154 | const allStates = notMeasured 155 | .concat(newStatesProj) 156 | .concat(newStatesPOVM) 157 | .filter((state) => state.vector.normSquared() > 1e-8) 158 | return new Measurement(allStates) 159 | } 160 | 161 | /** 162 | * Print outcomes. 163 | */ 164 | toString(): string { 165 | return this.states 166 | .map((state) => { 167 | const percent = 100 * state.vector.normSquared() 168 | const name = state.name.join('&') 169 | return `${percent.toFixed(1)}% [${name}] ${state.vector.normalize().toKetString()}` 170 | }) 171 | .join('\n') 172 | } 173 | 174 | /** 175 | * Randomly selects outcome, according to probabilities. 176 | */ 177 | pickRandom(): Measurement { 178 | const probs = this.states.map((state) => state.vector.normSquared()) 179 | const p = Math.random() 180 | let acc = 0 181 | for (let i = 0; i < this.states.length; i++) { 182 | acc += probs[i] 183 | if (acc > p) { 184 | const { name, vector } = this.states[i] 185 | return Measurement.fromVector(vector, name) 186 | } 187 | } 188 | throw new Error('Measurement probabilities does not sum up to 1.') 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/OperatorEntry.ts: -------------------------------------------------------------------------------- 1 | import Complex from './Complex' 2 | import { coordsFromIndex } from './helpers' 3 | 4 | /** 5 | * Class for operarator entires. 6 | * To be used only within a Operator object, or to create such. 7 | */ 8 | export default class OperatorEntry { 9 | readonly coordOut: readonly number[] 10 | readonly coordIn: readonly number[] 11 | readonly value: Complex 12 | 13 | /** 14 | * Creates a VectorEntry from output and input coordinates, and value. 15 | * @param coordOut 16 | * @param coordIn 17 | * @param value 18 | */ 19 | constructor(coordOut: readonly number[], coordIn: readonly number[], value: Complex) { 20 | this.coordOut = coordOut 21 | this.coordIn = coordIn 22 | this.value = value 23 | } 24 | 25 | /** 26 | * Tensor product of two entires (multiplies values, concatenates coordinates). 27 | * @param e2 The other entry 28 | */ 29 | outer(e2: OperatorEntry): OperatorEntry { 30 | const e1 = this 31 | return new OperatorEntry(e1.coordOut.concat(e2.coordOut), e1.coordIn.concat(e2.coordIn), e1.value.mul(e2.value)) 32 | } 33 | 34 | /** 35 | * Overrides toString() method. 36 | * @returms E.g. "Sparse operator entry [[3,0,1], [2,1,1]] has value (1.00 - 0.5 i)" 37 | */ 38 | toString(): string { 39 | return ( 40 | `Sparse operator entry [${this.coordOut.toString()}, ${this.coordIn.toString()}] ` + 41 | `has value ${this.value.toString()}` 42 | ) 43 | } 44 | 45 | /** 46 | * Entry coordinates in string representation. Can be used for hashing. 47 | */ 48 | coordKey(): string { 49 | return `${this.coordOut.toString()}-${this.coordIn.toString()}` 50 | } 51 | 52 | /** 53 | * Creates OperatorEntry from two integer indices, coordinate sizes and a value. 54 | * @param indexOut an integer for output index 55 | * @param indexIn an integer for output index 56 | * @param sizesOut sizes od output dimensions 57 | * @param sizesIn sizes od output dimensions 58 | * @param value entry value 59 | */ 60 | static fromIndexIndexValue( 61 | indexOut: number, 62 | indexIn: number, 63 | sizesOut: number[], 64 | sizesIn: number[], 65 | value: Complex, 66 | ): OperatorEntry { 67 | const coordOut = coordsFromIndex(indexOut, sizesOut) 68 | const coordIn = coordsFromIndex(indexIn, sizesIn) 69 | return new OperatorEntry(coordOut, coordIn, value) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Ops.ts: -------------------------------------------------------------------------------- 1 | import Complex, { Cx } from './Complex' 2 | import Vector from './Vector' 3 | import Operator from './Operator' 4 | import Dimension from './Dimension' 5 | import { TAU } from './Constants' 6 | 7 | const dimPol = Dimension.polarization() 8 | const dimDir = Dimension.direction() 9 | const idPol = Operator.identity([dimPol]) 10 | const idDir = Operator.identity([dimDir]) 11 | 12 | const cos = (alpha: number): Complex => Cx(Math.cos(alpha), 0) 13 | const sin = (alpha: number): Complex => Cx(Math.sin(alpha), 0) 14 | const mod = (x: number, n: number): number => ((x % n) + n) % n 15 | 16 | // not as fast as this one: https://en.wikipedia.org/wiki/Fast_inverse_square_root 17 | export const isqrt2 = Cx(Math.SQRT1_2) 18 | 19 | const toPolVec = (array: Complex[]): Vector => Vector.fromArray(array, [dimPol]).normalize() 20 | 21 | /** 22 | * Vectors for polarization states: H, V, D, A, L, R. 23 | */ 24 | export const polStates: Record = { 25 | H: toPolVec([Cx(1), Cx(0)]), 26 | V: toPolVec([Cx(0), Cx(1)]), 27 | D: toPolVec([Cx(1), Cx(1)]), 28 | A: toPolVec([Cx(-1), Cx(1)]), 29 | L: toPolVec([Cx(1), Cx(0, 1)]), 30 | R: toPolVec([Cx(1), Cx(0, -1)]), 31 | } 32 | 33 | /** 34 | * A 2d matrix, a rotation for complex numbers. 35 | * @param alpha An angle, in radians, i.e. from the range [0, Tau]. 36 | * @param dimension A dimension of size 2, e.g. spin or polarization. 37 | */ 38 | export function rotationMatrix(alpha: number, dimension: Dimension): Operator { 39 | const array = [ 40 | [cos(alpha), sin(-alpha)], 41 | [sin(alpha), cos(alpha)], 42 | ] 43 | return Operator.fromArray(array, [dimension]) 44 | } 45 | 46 | /** 47 | * A 2d matrix, a projection for complex numbers. 48 | * @param alpha An angle, in radians, i.e. from the range [0, Tau]. 49 | * @param dimension A dimension of size 2, e.g. spin or polarization. 50 | */ 51 | export function projectionMatrix(alpha: number, dimension: Dimension): Operator { 52 | const array = [ 53 | [cos(alpha).mul(cos(alpha)), cos(alpha).mul(sin(alpha))], 54 | [cos(alpha).mul(sin(alpha)), sin(alpha).mul(sin(alpha))], 55 | ] 56 | return Operator.fromArray(array, [dimension]) 57 | } 58 | 59 | /** 60 | * A 2d matrix, phase shift between projections. For phase plate. 61 | * @param alpha An angle, in radians, i.e. from the range [0, Tau]. 62 | * @param phase Phase shift for angle as for the main state, [0, 1]. 63 | * @param phaseOrthogonal Phase shift for for the orthogonal state, [0, 1]. 64 | * @param dimension A dimension of size 2, e.g. spin or polarization. 65 | */ 66 | export function phaseShiftForRealEigenvectors( 67 | alpha: number, 68 | phase: number, 69 | phaseOrthogonal: number, 70 | dimension: Dimension, 71 | ): Operator { 72 | return Operator.add([ 73 | projectionMatrix(alpha, dimension).mulConstant(Complex.fromPolar(1, phase * TAU)), 74 | projectionMatrix(alpha + 0.25 * TAU, dimension).mulConstant(Complex.fromPolar(1, phaseOrthogonal * TAU)), 75 | ]) 76 | } 77 | 78 | /** 79 | * Reflection from an optically lighter material. 80 | * Note that change horizontal frame of reference. 81 | */ 82 | export function reflectPhaseFromLighter(): Operator { 83 | const array = [ 84 | [Cx(-1), Cx(0)], 85 | [Cx(0), Cx(1)], 86 | ] 87 | return Operator.fromArray(array, [dimPol], [dimPol]) 88 | } 89 | 90 | /** 91 | * Reflection from an optically denser material. 92 | * Note that change horizontal frame of reference. 93 | */ 94 | export function reflectPhaseFromDenser(): Operator { 95 | const array = [ 96 | [Cx(1), Cx(0)], 97 | [Cx(0), Cx(-1)], 98 | ] 99 | return Operator.fromArray(array, [dimPol], [dimPol]) 100 | } 101 | 102 | /** 103 | * An omnidirectional operator multiplying by a complex number. 104 | * @param r Absolute value of amplitide multipier. E.g. Math.SQRT1_2 for 105 | * @param rot Phase multiplier, in TAU (from range: [0,1]). 106 | */ 107 | export function amplitudeIntensity(r: number, rot: number): Operator { 108 | return Operator.outer([idDir, idPol]).mulConstant(Complex.fromPolar(r, TAU * rot)) 109 | } 110 | 111 | /** 112 | * A reflection from a plane that has two refletive sides. 113 | * Rotations: - / | \ 114 | * @param angle In degrees, only values [0, 45, 90, 135]. From ->, counterclockwise. 115 | * @returns Operator with dimensions [Dimension.polarization()] 116 | */ 117 | export function reflectFromPlaneDirection(angle: number): Operator { 118 | let sparseCoords: [string, string, Complex][] 119 | switch (mod(angle, 180)) { 120 | case 0: // - 121 | sparseCoords = [ 122 | ['v', '^', Cx(1)], 123 | ['^', 'v', Cx(1)], 124 | ] 125 | break 126 | case 45: // / 127 | sparseCoords = [ 128 | ['^', '>', Cx(1)], 129 | ['>', '^', Cx(1)], 130 | ['v', '<', Cx(1)], 131 | ['<', 'v', Cx(1)], 132 | ] 133 | break 134 | case 90: // | 135 | sparseCoords = [ 136 | ['<', '>', Cx(1)], 137 | ['>', '<', Cx(1)], 138 | ] 139 | break 140 | case 135: // \ 141 | sparseCoords = [ 142 | ['v', '>', Cx(1)], 143 | ['>', 'v', Cx(1)], 144 | ['^', '<', Cx(1)], 145 | ['<', '^', Cx(1)], 146 | ] 147 | break 148 | default: 149 | throw new Error(`Angle ${angle} % 180 isn't in the set [0, 45, 90, 135]`) 150 | } 151 | return Operator.fromSparseCoordNames(sparseCoords, [dimDir]) 152 | } 153 | 154 | /** 155 | * An auxiliary operation for beam splitter transmittion directions. 156 | * @param angle Angle in degrees [0, 45, 90, 135] up to 180. --> and CCW. 157 | * @returns Operator with dimensions [Dimension.direction()]. 158 | */ 159 | export function beamsplitterTransmittionDirections(angle: number): Operator { 160 | switch (mod(angle, 180)) { 161 | case 0: // - 162 | return Operator.fromSparseCoordNames( 163 | [ 164 | ['^', '^', Cx(1)], 165 | ['v', 'v', Cx(1)], 166 | ], 167 | [dimDir], 168 | ) 169 | case 45: // / 170 | case 135: // \ 171 | return idDir 172 | case 90: // | 173 | return Operator.fromSparseCoordNames( 174 | [ 175 | ['>', '>', Cx(1)], 176 | ['<', '<', Cx(1)], 177 | ], 178 | [dimDir], 179 | ) 180 | default: 181 | throw new Error(`Angle ${angle} % 180 isn't in the set [0, 45, 90, 135].`) 182 | } 183 | } 184 | 185 | const diodeRight = Operator.fromSparseCoordNames([['>', '>', Cx(1)]], [dimDir]) 186 | const diodeUp = Operator.fromSparseCoordNames([['^', '^', Cx(1)]], [dimDir]) 187 | const diodeLeft = Operator.fromSparseCoordNames([['<', '<', Cx(1)]], [dimDir]) 188 | const diodeDown = Operator.fromSparseCoordNames([['v', 'v', Cx(1)]], [dimDir]) 189 | 190 | /** 191 | * An auxiliary operation for constructing other directional operators. 192 | * @param angle Angle in degrees [0, 90, 180, 270] up to 360. --> and CCW. 193 | * @returns Operator with dimensions [Dimension.direction()]. 194 | */ 195 | export function diodeForDirections(angle: number): Operator { 196 | switch (mod(angle, 360)) { 197 | case 0: // -> 198 | return diodeRight 199 | case 90: // ^ 200 | return diodeUp 201 | case 180: // <- 202 | return diodeLeft 203 | case 270: // v 204 | return diodeDown 205 | default: 206 | throw new Error(`Angle ${angle} % 360 isn't in the set [0, 90, 180, 270].`) 207 | } 208 | } 209 | 210 | // TODO: 211 | // Add in elements "projection on" 212 | -------------------------------------------------------------------------------- /src/Simulation.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Operator from './Operator' 3 | import Frame from './Frame' 4 | import { generateOperators } from './Elements' 5 | import { weightedRandomInt, startingPolarization, startingDirection } from './helpers' 6 | import { IAbsorption, IGrid, ICell, IIndicator, IXYOperator, IParticle, Elem } from './interfaces' 7 | 8 | /** 9 | * Generate the laser indicator from the grid laser cell 10 | * @returns laserIndicator 11 | */ 12 | export function generateLaserIndicator(cells: ICell[]): IIndicator { 13 | const lasers = cells.filter((cell: ICell) => cell.element === Elem.Laser) 14 | if (lasers.length !== 1) { 15 | throw new Error(`Cannot initialize QuantumSimulation. ${lasers.length} != 1 lasers.`) 16 | } 17 | const laserIndicator: IIndicator = { 18 | x: lasers[0].x, 19 | y: lasers[0].y, 20 | direction: startingDirection(lasers[0].rotation), 21 | polarization: startingPolarization(lasers[0].polarization), 22 | } 23 | return laserIndicator 24 | } 25 | 26 | /** 27 | * SIMULATION CLASS 28 | * Loads a grid 29 | * Convert its elements into operators and merge them into a global operator. 30 | * Generate simulation frames and absorptions. 31 | */ 32 | export default class Simulation { 33 | readonly sizeX: number 34 | readonly sizeY: number 35 | public operators: IXYOperator[] 36 | public globalOperator: Operator 37 | public frames: Frame[] 38 | 39 | public constructor(sizeX: number, sizeY: number, operators: IXYOperator[]) { 40 | this.sizeX = sizeX 41 | this.sizeY = sizeY 42 | this.operators = operators 43 | this.globalOperator = Frame.singlePhotonInteraction(this.sizeX, this.sizeY, this.operators) 44 | this.frames = [] 45 | } 46 | 47 | public static fromGrid(grid: IGrid): Simulation { 48 | return new Simulation(grid.cols, grid.rows, generateOperators(grid.cells)) 49 | } 50 | 51 | /** 52 | * Initialize simulation from indicator 53 | * @param indicator IIndicator 54 | */ 55 | public initializeFromIndicator(indicator: IIndicator): void { 56 | this.frames = [] 57 | const frame = new Frame(this) 58 | frame.addPhotonFromIndicator(indicator.x, indicator.y, indicator.direction, indicator.polarization) 59 | this.frames.push(frame) 60 | } 61 | 62 | /** 63 | * Get last simulation frame 64 | * @returns last frame 65 | */ 66 | public get lastFrame(): Frame { 67 | return this.frames[this.frames.length - 1] 68 | } 69 | 70 | /** 71 | * Compute the next simulation frame 72 | * @returns computed frame 73 | */ 74 | public nextFrame(): Frame { 75 | if (this.frames.length === 0) { 76 | throw new Error(`Cannot do nextFrame when there are no frames. initializeFromLaser or something else.`) 77 | } 78 | const frame = new Frame(this, this.lastFrame.vector) 79 | frame.propagateAndInteract() 80 | return frame 81 | } 82 | 83 | /** 84 | * Compute next frames until probability threshold 85 | * @param n default number of frames 86 | * @param stopThreshold stop if probability below threshold 87 | * @param logging toggle console debug 88 | */ 89 | public generateFrames(n = 20, stopThreshold = 1e-6, logging = false): void { 90 | for (let i = 0; i < n; i += 1) { 91 | const nextFrame = this.nextFrame() 92 | this.frames.push(nextFrame) 93 | if (this.lastFrame.probability < stopThreshold) { 94 | break 95 | } 96 | } 97 | if (logging) { 98 | console.debug('POST-SIMULATION LOG:') 99 | console.debug('probabilityPerFrame', this.probabilityPerFrame) 100 | console.debug('totalAbsorptionPerFrame', this.totalAbsorptionPerFrame) 101 | console.debug('totalAbsorptionPerTile', this.totalAbsorptionPerTile) 102 | console.debug('An example of realization:') 103 | // const randomSample = this.sampleRandomRealization(); 104 | // randomSample.statePerFrame.forEach((state) => console.debug(state.ketString())); 105 | // console.debug( 106 | // `Detected: in ${randomSample.fate.name} at (${randomSample.fate.x},${randomSample.fate.y})` 107 | // ); 108 | } 109 | } 110 | 111 | /** 112 | * Quantum state probability for each frame. 113 | * @returns probability of frame 114 | */ 115 | public get probabilityPerFrame(): number[] { 116 | return this.frames.map((frame): number => frame.probability) 117 | } 118 | 119 | /** 120 | * Quantum state probability of absorption for each frame. 121 | */ 122 | public get totalAbsorptionPerFrame(): number[] { 123 | return this.frames.map((frame): number => frame.totalProbabilityLoss) 124 | } 125 | 126 | /** 127 | * Retrieve a list of all the particles for quantum path computation 128 | * @returns particle list 129 | */ 130 | public get allParticles(): IParticle[] { 131 | const result: IParticle[] = [] 132 | this.frames.forEach((frame): void => { 133 | frame.particles.forEach((particle): void => { 134 | result.push(particle) 135 | }) 136 | }) 137 | return result 138 | } 139 | 140 | /** 141 | * Total (summed over all frames) absorption per tile. 142 | * {x: -1, y: -1, probability: ...} means falling of the board. 143 | * @todo If needed, I we can add exact (off-board) coordinates of all lost photons. 144 | * @returns E.g. 145 | * [{x: 2, y: 1, probability: 0.25}, {x: 3, y: 5, probability: 0.25}, {x: -1, y: -1, probability: 0.25}] 146 | */ 147 | public get totalAbsorptionPerTile(): IAbsorption[] { 148 | return _(this.frames) 149 | .flatMap((frame): IAbsorption[] => frame.absorptions) 150 | .groupBy((absorption: IAbsorption): string => `(${absorption.x}.${absorption.y})`) 151 | .values() 152 | .map( 153 | (absorptions): IAbsorption => ({ 154 | x: absorptions[0].x, 155 | y: absorptions[0].y, 156 | probability: _.sumBy(absorptions, 'probability'), 157 | }), 158 | ) 159 | .value() 160 | } 161 | 162 | /** 163 | * Create a random realization. So - the state is normalized, until a successful measurement. 164 | * @remark So far for 1 particle. 165 | * @todo Make it work for more particles. 166 | * @todo Maybe make it another object? Or use QuantumFrame? 167 | * @todo Kinda ugly return 168 | */ 169 | public sampleRandomRealization(): { 170 | statePerFrame: Frame[] 171 | probability: number 172 | step: number 173 | x: number 174 | y: number 175 | // eslint-disable-next-line indent 176 | } { 177 | // first, which frame 178 | const lastId = weightedRandomInt(this.totalAbsorptionPerFrame, false) 179 | // -1 if no measurement, and we need to deal with that 180 | const lastFrameAbs = this.frames[lastId].absorptions 181 | const absorptionId = weightedRandomInt( 182 | lastFrameAbs.map((d): number => d.probability), 183 | true, 184 | ) 185 | const absorption = lastFrameAbs[absorptionId] 186 | const states = this.frames.slice(0, lastId).map((frame): Frame => frame.normalize()) 187 | 188 | return { 189 | statePerFrame: states, 190 | probability: absorption.probability, 191 | step: lastId, 192 | x: absorption.x, 193 | y: absorption.y, 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/VectorEntry.ts: -------------------------------------------------------------------------------- 1 | import Complex from './Complex' 2 | import { coordsFromIndex } from './helpers' 3 | 4 | /** 5 | * Class for vector entires (also know as: vector values, cells). 6 | * To be used only within a Vector object, or to create such. 7 | */ 8 | export default class VectorEntry { 9 | readonly coord: readonly number[] 10 | readonly value: Complex 11 | 12 | /** 13 | * Creates a VectorEntry from coord and value. 14 | * @param coord 15 | * @param value 16 | */ 17 | constructor(coord: readonly number[], value: Complex) { 18 | this.coord = coord 19 | this.value = value 20 | } 21 | 22 | /** 23 | * Tensor product of two entires (multiplies values, concatenates coordinates). 24 | * @param e2 The other entry 25 | */ 26 | outer(e2: VectorEntry): VectorEntry { 27 | const e1 = this 28 | return new VectorEntry(e1.coord.concat(e2.coord), e1.value.mul(e2.value)) 29 | } 30 | 31 | /** 32 | * Overrides toString() method. 33 | * @returms E.g. "Sparse vector entry [3,0,1] has value (1.00 - 0.50 i)" 34 | */ 35 | toString(): string { 36 | return `Sparse vector entry [${this.coord.toString()}] has value ${this.value.toString()}` 37 | } 38 | 39 | /** 40 | * Creates a VectorEntry from an integer index, coordinate sizes and value. 41 | * @param index an integer 42 | * @param sizes sizes od dimensions 43 | * @param value entry value 44 | */ 45 | static fromIndexValue(index: number, sizes: number[], value: Complex): VectorEntry { 46 | const coords = coordsFromIndex(index, sizes) 47 | return new VectorEntry(coords, value) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line */ 2 | import _ from 'lodash' 3 | import { PolEnum, DirEnum } from './interfaces' 4 | 5 | /** 6 | * Turns an index into a multi-index, according to dimension sizes. 7 | * @note It uses big-endian, 8 | * https://chortle.ccsu.edu/AssemblyTutorial/Chapter-15/ass15_3.html. 9 | * Until 0.2.12 it used small endian. 10 | * @param index An integer 11 | * @param sizes Sizes of each dimension 12 | * 13 | * @returns Index in each dimension 14 | */ 15 | export function coordsFromIndex(index: number, sizes: number[]): number[] { 16 | let i = index 17 | const coords = [...sizes].reverse().map((dimSize) => { 18 | const coord = i % dimSize 19 | i = (i - coord) / dimSize 20 | return coord 21 | }) 22 | return coords.reverse() 23 | } 24 | 25 | /** 26 | * Turns a multi-index into an index, inverse of {@link coordsFromIndex} 27 | * @note It uses big-endian, 28 | * https://chortle.ccsu.edu/AssemblyTutorial/Chapter-15/ass15_3.html. 29 | * Until 0.2.12 it used small endian. 30 | * @param coords Index in each dimension 31 | * @param sizes Sizes of each dimension 32 | * 33 | * @return Index 34 | */ 35 | export function coordsToIndex(coords: readonly number[], sizes: readonly number[]): number { 36 | if (coords.length !== sizes.length) { 37 | throw new Error(`Coordinates ${coords} and sizes ${sizes} are of different lengths}.`) 38 | } 39 | const coordsRev = [...coords].reverse() 40 | const sizesRev = [...sizes].reverse() 41 | 42 | let factor = 1 43 | let res = 0 44 | coordsRev.forEach((coord, dim) => { 45 | res += factor * coord 46 | factor *= sizesRev[dim] 47 | }) 48 | return res 49 | } 50 | 51 | /** 52 | * Ensures that coords and dimensions sizes are compatible 53 | * @param coords [c1, c2, ...] Coords, e.g. from VectorEntry or OperatorEntry 54 | * @param sizes [s1, s2, ...] Dimensions sizes 55 | * @returns Error when not [0 <= c1 < s1, 0 <= c2 < s2, ...] 56 | */ 57 | export function checkCoordsSizesCompability(coords: readonly number[], sizes: readonly number[]): void { 58 | if (coords.length !== sizes.length) { 59 | throw new Error(`Coordinates [${coords}] incompatible with sizes [${sizes}].`) 60 | } 61 | 62 | for (let i = 0; i < coords.length; i++) { 63 | const c = coords[i] 64 | if (c < 0 || c >= sizes[i]) { 65 | throw new Error(`Coordinates [${coords}] incompatible with sizes [${sizes}].`) 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Checks if a given array is a permuation, i.e. consist of [0, 1, 2, ..., n - 1] in any order. 72 | * @param array Array to be tested 73 | * @param n Number of elements 74 | */ 75 | export function isPermutation(array: number[], n = array.length): boolean { 76 | if (array.length !== n) { 77 | return false 78 | } 79 | const counts = new Array(array.length).fill(0) 80 | array.forEach((x) => (counts[x] += 1)) 81 | return _.every(counts) 82 | } 83 | 84 | /** 85 | * Creates complement indices, sorted. 86 | * @param indices E.g. [3, 1] 87 | * @param n E.g. 5 88 | * @return E.g. [0, 2, 4] 89 | */ 90 | export function indicesComplement(indices: readonly number[], n: number): number[] { 91 | const res = _.range(n).filter((i) => !_.includes(indices, i)) 92 | if (!isPermutation(indices.concat(res))) { 93 | throw new Error(`In [${indices}] are not unique integer, between 0 and ${n - 1}.`) 94 | } 95 | return res 96 | } 97 | 98 | /** 99 | * A function to merge coordinates. 100 | * @param coordIndices E.g. [3, 1] 101 | * @param complementIndices E.g. [0, 2, 4] 102 | * @returns A function that for [2, 3, 5], [7, 11] -> [2, 11, 3, 7, 5] 103 | */ 104 | export function joinCoordsFunc(coordIndices: readonly number[], complementIndices: readonly number[]) { 105 | return (coordGroup: readonly number[], coordContraction: readonly number[]): number[] => { 106 | const coord = new Array(coordIndices.length + complementIndices.length) 107 | coordGroup.forEach((c, i) => { 108 | coord[complementIndices[i]] = c 109 | }) 110 | coordContraction.forEach((c, i) => { 111 | coord[coordIndices[i]] = c 112 | }) 113 | return coord 114 | } 115 | } 116 | 117 | /** 118 | * Stolen from https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex 119 | * Alternatively: d3.hsl 120 | */ 121 | export function hslToHex(hParam: number, sParam: number, lParam: number): string { 122 | let h = hParam 123 | let s = sParam 124 | let l = lParam 125 | h /= 360 126 | s /= 100 127 | l /= 100 128 | let r 129 | let g 130 | let b 131 | if (s === 0) { 132 | r = l 133 | g = l 134 | b = l // achromatic 135 | } else { 136 | // eslint-disable-next-line 137 | const hue2rgb = (pParam: number, qParam: number, tParam: number) => { 138 | const p = pParam 139 | const q = qParam 140 | let t = tParam 141 | if (t < 0) t += 1 142 | if (t > 1) t -= 1 143 | if (t < 1 / 6) return p + (q - p) * 6 * t 144 | if (t < 1 / 2) return q 145 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 146 | return p 147 | } 148 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s 149 | const p = 2 * l - q 150 | r = hue2rgb(p, q, h + 1 / 3) 151 | g = hue2rgb(p, q, h) 152 | b = hue2rgb(p, q, h - 1 / 3) 153 | } 154 | // eslint-disable-next-line 155 | const toHex = (x: number) => { 156 | const hex = Math.round(x * 255).toString(16) 157 | return hex.length === 1 ? `0${hex}` : hex 158 | } 159 | return `#${toHex(r)}${toHex(g)}${toHex(b)}` 160 | } 161 | 162 | /** 163 | * Pick a random index of an array according to weights. 164 | * @param weights An array of weights. By default they should sum up to 1. 165 | * @param normalize If to normalize array. 166 | * @returns A number [0, ..., weights.length -1]. 167 | */ 168 | export function weightedRandomInt(weights: number[], normalize = true): number { 169 | let r = Math.random() 170 | if (normalize) { 171 | r *= weights.reduce((a, b): number => a + b, 0) 172 | } 173 | let cumSum = 0 174 | for (let i = 0; i < weights.length; i += 1) { 175 | cumSum += weights[i] 176 | if (cumSum > r) { 177 | return i 178 | } 179 | } 180 | return -1 181 | } 182 | 183 | /** 184 | * Output an enum describing laser starting polarization 185 | * @remark Moved from QG2 186 | * @returns a string enum 187 | */ 188 | export function startingPolarization(polarization: number): PolEnum { 189 | switch (polarization) { 190 | case 0: 191 | case 180: 192 | return PolEnum.H 193 | case 90: 194 | case 270: 195 | return PolEnum.V 196 | default: 197 | throw new Error(`Wrong starting polarization: ${polarization}`) 198 | } 199 | } 200 | 201 | /** 202 | * Output an enum describing laser starting polarization 203 | * @remark Moved from QG2 204 | * @returns a string enum 205 | */ 206 | export function startingDirection(rotation: number): DirEnum { 207 | switch (rotation) { 208 | case 0: 209 | return DirEnum['>'] 210 | case 90: 211 | return DirEnum['^'] 212 | case 180: 213 | return DirEnum['<'] 214 | case 270: 215 | return DirEnum.v 216 | default: 217 | throw new Error(`Wrong starting direction: ${rotation}`) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dimension } from './Dimension' 2 | export { default as Vector } from './Vector' 3 | export { default as Operator } from './Operator' 4 | export { default as Entanglement } from './Entanglement' 5 | export { default as Photons } from './Photons' 6 | export { default as Frame } from './Frame' 7 | export { default as Simulation } from './Simulation' 8 | export { Cx, default as Complex } from './Complex' 9 | export { default as VectorEntry } from './VectorEntry' 10 | export { default as OperatorEntry } from './OperatorEntry' 11 | export { default as Basis } from './Basis' 12 | export { default as Circuit } from './Circuit' 13 | import * as helpers from './helpers' 14 | import * as interfaces from './interfaces' 15 | export * from './Constants' 16 | import * as Elements from './Elements' 17 | import * as Ops from './Ops' 18 | import * as Gates from './Gates' 19 | export { helpers, interfaces, Elements, Ops, Gates } 20 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import Complex from './Complex' 2 | import Vector from './Vector' 3 | import Operator from './Operator' 4 | 5 | /** 6 | * PARTICLE INTERFACE 7 | * Particle interface in primitives 8 | */ 9 | export interface IParticle { 10 | x: number 11 | y: number 12 | direction: number 13 | are: number 14 | aim: number 15 | bre: number 16 | bim: number 17 | } 18 | 19 | /** 20 | * A newer version of {@link IParticle} 21 | */ 22 | export interface IPolarization { 23 | x: number 24 | y: number 25 | direction: number 26 | h: Complex 27 | v: Complex 28 | } 29 | 30 | /** 31 | * For turning Operator in a sparse array of rows of columns 32 | */ 33 | export interface IColumnOrRow { 34 | coord: readonly number[] 35 | vector: Vector 36 | } 37 | 38 | /** 39 | * For flat VectorEntry exports. 40 | */ 41 | export interface IEntryIndexValue { 42 | i: number 43 | v: Complex 44 | } 45 | 46 | /** 47 | * For flat MatrixEntry exports. 48 | */ 49 | export interface IEntryIndexIndexValue { 50 | i: number 51 | j: number 52 | v: Complex 53 | } 54 | 55 | /** 56 | * For basis changes. 57 | */ 58 | export interface INamedVector { 59 | name: string 60 | vector: Vector 61 | } 62 | 63 | /** 64 | * For position (x, y) and operator with direction and polarization dimensions. 65 | */ 66 | export interface IXYOperator { 67 | x: number 68 | y: number 69 | op: Operator 70 | } 71 | 72 | /** 73 | * And interface for visualizing kets. Serializes as amplitudes, and an array of coord strings. 74 | */ 75 | export interface IKetComponent { 76 | amplitude: Complex 77 | coordStrs: string[] 78 | } 79 | 80 | /** 81 | * Interface for localized operators 82 | */ 83 | export interface ITileIntensity { 84 | x: number 85 | y: number 86 | probability: number 87 | } 88 | 89 | /** 90 | * Interface used for goals and absorptions 91 | */ 92 | export interface IAbsorption { 93 | x: number 94 | y: number 95 | probability: number 96 | } 97 | 98 | /** 99 | * @todo Duplicate naming is confusing 100 | */ 101 | export interface IKetComponentFrame { 102 | amplitude: Complex 103 | particleCoords: IParticleCoord[] 104 | } 105 | 106 | // TODO: Should come as enum in a nicer format 107 | export interface IParticleCoord { 108 | kind: string // for now only 'photon' 109 | x: number 110 | y: number 111 | dir: number // 0: > 1: ^, 2: <. 3: v 112 | pol: number // 0: H, 1: V 113 | } 114 | 115 | /** 116 | * Grid interface in primitives 117 | */ 118 | export interface IGrid { 119 | cols: number 120 | rows: number 121 | cells: ICell[] 122 | } 123 | /* eslint-disable */ 124 | 125 | /** 126 | * Cell interface in primitives 127 | */ 128 | export interface ICell { 129 | x: number 130 | y: number 131 | element: string 132 | rotation: number 133 | polarization: number 134 | strength?: number 135 | split?: number 136 | } 137 | 138 | /** 139 | * Photon indicator interface for glue code with qt Photons 140 | * @deprecated 141 | */ 142 | export interface IIndicator { 143 | x: number 144 | y: number 145 | direction: DirEnum 146 | polarization: PolEnum 147 | } 148 | 149 | /** 150 | * Temporary interface for localized operators and grid informations 151 | */ 152 | export interface IOperatorGrid { 153 | sizeX: number 154 | sizeY: number 155 | operators: IXYOperator[] 156 | globalOperator: Operator 157 | } 158 | 159 | /** 160 | * Laser starting polarization enum 161 | */ 162 | export const enum PolEnum { 163 | V = 'V', 164 | H = 'H', 165 | } 166 | 167 | /** 168 | * Laser starting direction enum 169 | */ 170 | export const enum DirEnum { 171 | '>' = '>', 172 | '^' = '^', 173 | '<' = '<', 174 | 'v' = 'v', 175 | } 176 | 177 | /** 178 | * List of element names 179 | */ 180 | export enum Elem { 181 | // Basic 182 | Void = 'Void', 183 | Wall = 'Wall', 184 | Gate = 'Gate', 185 | // Source 186 | Laser = 'Laser', 187 | NonLinearCrystal = 'NonLinearCrystal', 188 | // Direction 189 | Mirror = 'Mirror', 190 | BeamSplitter = 'BeamSplitter', 191 | PolarizingBeamSplitter = 'PolarizingBeamSplitter', 192 | CoatedBeamSplitter = 'CoatedBeamSplitter', 193 | CornerCube = 'CornerCube', 194 | // Absorption 195 | Detector = 'Detector', 196 | Rock = 'Rock', 197 | Mine = 'Mine', 198 | Absorber = 'Absorber', 199 | DetectorFour = 'DetectorFour', 200 | // Polarization 201 | Polarizer = 'Polarizer', 202 | QuarterWavePlate = 'QuarterWavePlate', 203 | HalfWavePlate = 'HalfWavePlate', 204 | SugarSolution = 'SugarSolution', 205 | FaradayRotator = 'FaradayRotator', 206 | // Phase 207 | Glass = 'Glass', 208 | VacuumJar = 'VacuumJar', 209 | } 210 | -------------------------------------------------------------------------------- /tests/Basis.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import Basis from '../src/Basis' 4 | import Vector from '../src/Vector' 5 | import * as Elements from '../src/Elements' 6 | import './customMatchers' 7 | 8 | describe('Basis', () => { 9 | it('creates bases for polarization', () => { 10 | expect(Basis.polarization('HV').toString()).toBe( 11 | 'Basis HV for dimension polarization\n|H⟩ = (1.00 +0.00i) |H⟩\n|V⟩ = (1.00 +0.00i) |V⟩', 12 | ) 13 | expect(Basis.polarization('DA').toString()).toBe( 14 | 'Basis DA for dimension polarization\n' + 15 | '|D⟩ = (0.71 +0.00i) |H⟩ + (0.71 +0.00i) |V⟩\n' + 16 | '|A⟩ = (-0.71 +0.00i) |H⟩ + (0.71 +0.00i) |V⟩', 17 | ) 18 | expect(Basis.polarization('LR').toString()).toBe( 19 | 'Basis LR for dimension polarization\n' + 20 | '|L⟩ = (0.71 +0.00i) |H⟩ + (0.00 +0.71i) |V⟩\n' + 21 | '|R⟩ = (0.00 +0.71i) |H⟩ + (0.71 +0.00i) |V⟩', 22 | ) 23 | 24 | expect(() => Basis.polarization('VH')).toThrowError('') 25 | }) 26 | 27 | it('creates bases for spin', () => { 28 | expect(Basis.spin('spin-z').toString()).toBe( 29 | 'Basis ud for dimension spin\n|u⟩ = (1.00 +0.00i) |u⟩\n|d⟩ = (1.00 +0.00i) |d⟩', 30 | ) 31 | expect(Basis.spin('spin-x').toString()).toBe( 32 | 'Basis uxdx for dimension spin\n' + 33 | '|ux⟩ = (0.71 +0.00i) |u⟩ + (0.71 +0.00i) |d⟩\n' + 34 | '|dx⟩ = (-0.71 +0.00i) |u⟩ + (0.71 +0.00i) |d⟩', 35 | ) 36 | expect(Basis.spin('spin-y').toString()).toBe( 37 | 'Basis uydy for dimension spin\n' + 38 | '|uy⟩ = (0.71 +0.00i) |u⟩ + (0.00 +0.71i) |d⟩\n' + 39 | '|dy⟩ = (0.00 +0.71i) |u⟩ + (0.71 +0.00i) |d⟩', 40 | ) 41 | 42 | expect(() => Basis.spin('du')).toThrowError('') 43 | }) 44 | 45 | // tests for qubit 46 | 47 | it('polarization: changing to the same basis is identity', () => { 48 | const polHV = Basis.polarization('HV') 49 | const polDA = Basis.polarization('DA') 50 | const polLR = Basis.polarization('LR') 51 | 52 | expect(polHV.basisChangeUnitary(polHV).isCloseToIdentity()).toBe(true) 53 | expect(polDA.basisChangeUnitary(polDA).isCloseToIdentity()).toBe(true) 54 | expect(polLR.basisChangeUnitary(polLR).isCloseToIdentity()).toBe(true) 55 | }) 56 | 57 | it('polarization: changing to another basis is unitary but not identity', () => { 58 | const polHV = Basis.polarization('HV') 59 | const polDA = Basis.polarization('DA') 60 | const polLR = Basis.polarization('LR') 61 | 62 | expect(polHV.basisChangeUnitary(polDA).isCloseToUnitary()).toBe(true) 63 | expect(polDA.basisChangeUnitary(polLR).isCloseToUnitary()).toBe(true) 64 | expect(polLR.basisChangeUnitary(polHV).isCloseToUnitary()).toBe(true) 65 | 66 | // I cannot use isCloseToUnitary, as the dimensions are different! 67 | const idValues = [ 68 | [Cx(1), Cx(0)], 69 | [Cx(0), Cx(1)], 70 | ] 71 | expect(polHV.basisChangeUnitary(polDA)).not.operatorCloseToNumbers(idValues) 72 | expect(polDA.basisChangeUnitary(polLR)).not.operatorCloseToNumbers(idValues) 73 | expect(polLR.basisChangeUnitary(polHV)).not.operatorCloseToNumbers(idValues) 74 | }) 75 | 76 | it('polarization: dag changes the order', () => { 77 | const polHV = Basis.polarization('HV') 78 | const polDA = Basis.polarization('DA') 79 | const polLR = Basis.polarization('LR') 80 | 81 | expect(polHV.basisChangeUnitary(polDA).dag()).operatorCloseToNumbers(polDA.basisChangeUnitary(polHV).toDense()) 82 | expect(polDA.basisChangeUnitary(polLR).dag()).operatorCloseToNumbers(polLR.basisChangeUnitary(polDA).toDense()) 83 | expect(polLR.basisChangeUnitary(polHV).dag()).operatorCloseToNumbers(polHV.basisChangeUnitary(polLR).toDense()) 84 | }) 85 | 86 | it('polarization: cyclic property', () => { 87 | const polHV = Basis.polarization('HV') 88 | const polDA = Basis.polarization('DA') 89 | const polLR = Basis.polarization('LR') 90 | 91 | const ab = polHV.basisChangeUnitary(polDA) 92 | const bc = polDA.basisChangeUnitary(polLR) 93 | const ca = polLR.basisChangeUnitary(polHV) 94 | 95 | const abbcca = ab.mulOp(bc).mulOp(ca) 96 | expect(abbcca.isCloseToIdentity()).toBe(true) 97 | }) 98 | 99 | it('spin: changing to the same basis is identity', () => { 100 | const spinX = Basis.spin('spin-x') 101 | const spinY = Basis.spin('spin-y') 102 | const spinZ = Basis.spin('spin-z') 103 | 104 | expect(spinX.basisChangeUnitary(spinX).isCloseToIdentity()).toBe(true) 105 | expect(spinY.basisChangeUnitary(spinY).isCloseToIdentity()).toBe(true) 106 | expect(spinZ.basisChangeUnitary(spinZ).isCloseToIdentity()).toBe(true) 107 | }) 108 | 109 | it('basis change unitary from dimension', () => { 110 | const polLR = Basis.polarization('LR') 111 | const toLRfromDA = polLR.basisChangeUnitaryFromDimension(Dimension.polarization(['D', 'A'])) 112 | expect(toLRfromDA.dimensionsOut[0].name).toEqual('polarization') 113 | expect(toLRfromDA.dimensionsOut[0].coordString).toEqual('LR') 114 | expect(toLRfromDA.dimensionsIn[0].name).toEqual('polarization') 115 | expect(toLRfromDA.dimensionsIn[0].coordString).toEqual('DA') 116 | }) 117 | 118 | it('change all dims for vector', () => { 119 | const vector = Vector.fromSparseCoordNames( 120 | [ 121 | ['0u0HH', Cx(0.5)], 122 | ['0u0HV', Cx(0.5)], 123 | ['2u1VV', Cx(0.5)], 124 | ['2d1VV', Cx(0.0, -0.5)], 125 | ], 126 | [Dimension.position(5), Dimension.spin(), Dimension.qubit(), Dimension.polarization(), Dimension.polarization()], 127 | ) 128 | 129 | const polHV = Basis.polarization('HV') 130 | const polDA = Basis.polarization('DA') 131 | const spinY = Basis.spin('spin-y') 132 | expect(polHV.changeAllDimsOfVector(vector).toKetString('cartesian')).toEqual( 133 | '(0.50 +0.00i) |0,u,0,H,H⟩ + (0.50 +0.00i) |0,u,0,H,V⟩ + (0.50 +0.00i) |2,u,1,V,V⟩ + (0.00 -0.50i) |2,d,1,V,V⟩', 134 | ) 135 | expect(polDA.changeAllDimsOfVector(vector).toKetString('cartesian')).toEqual( 136 | '(0.50 +0.00i) |0,u,0,D,D⟩ + (-0.50 +0.00i) |0,u,0,A,D⟩ + (0.25 +0.00i) |2,u,1,D,D⟩ + (0.25 +0.00i) |2,u,1,D,A⟩' + 137 | ' + (0.25 +0.00i) |2,u,1,A,D⟩ + (0.25 +0.00i) |2,u,1,A,A⟩ + (0.00 -0.25i) |2,d,1,D,D⟩' + 138 | ' + (0.00 -0.25i) |2,d,1,D,A⟩ + (0.00 -0.25i) |2,d,1,A,D⟩ + (0.00 -0.25i) |2,d,1,A,A⟩', 139 | ) 140 | expect(spinY.changeAllDimsOfVector(vector).toKetString('cartesian')).toEqual( 141 | '(0.35 +0.00i) |0,uy,0,H,H⟩ + (0.35 +0.00i) |0,uy,0,H,V⟩ + (0.00 -0.35i) |0,dy,0,H,H⟩' + 142 | ' + (0.00 -0.35i) |0,dy,0,H,V⟩ + (0.00 -0.71i) |2,dy,1,V,V⟩', 143 | ) 144 | }) 145 | 146 | it('polarization: change all dims for operator', () => { 147 | const faradayRotator = Elements.faradayRotator(90) 148 | const polLR = Basis.polarization('LR') 149 | const rotatorRotated = polLR.changeAllDimsOfOperator(faradayRotator) 150 | expect(rotatorRotated.dimensionsOut[0].name).toEqual('direction') 151 | expect(rotatorRotated.dimensionsOut[1].name).toEqual('polarization') 152 | expect(rotatorRotated.dimensionsOut[1].coordString).toEqual('LR') 153 | expect(rotatorRotated.dimensionsIn[0].name).toEqual('direction') 154 | expect(rotatorRotated.dimensionsIn[1].name).toEqual('polarization') 155 | expect(rotatorRotated.dimensionsIn[1].coordString).toEqual('LR') 156 | expect(rotatorRotated.toString('polarTau', 2, ' + ', false)).toEqual( 157 | '1.00 exp(0.88τi) |^,L⟩⟨^,L| + 1.00 exp(0.13τi) |^,R⟩⟨^,R|' + 158 | ' + 1.00 exp(0.13τi) |v,L⟩⟨v,L| + 1.00 exp(0.88τi) |v,R⟩⟨v,R|', 159 | ) 160 | }) 161 | 162 | it('photon: singlet state same all bases', () => { 163 | const singlet = Vector.fromSparseCoordNames( 164 | [ 165 | ['HV', Cx(Math.SQRT1_2)], 166 | ['VH', Cx(-Math.SQRT1_2)], 167 | ], 168 | [Dimension.polarization(), Dimension.polarization()], 169 | ) 170 | 171 | expect(singlet.toBasisAll('polarization', 'DA').toKetString('cartesian')).toEqual( 172 | '(0.71 +0.00i) |D,A⟩ + (-0.71 +0.00i) |A,D⟩', 173 | ) 174 | expect(singlet.toBasisAll('polarization', 'LR').toKetString('cartesian')).toEqual( 175 | '(0.71 +0.00i) |L,R⟩ + (-0.71 +0.00i) |R,L⟩', 176 | ) 177 | }) 178 | 179 | it('spin singlet state same all bases', () => { 180 | const singlet = Vector.fromSparseCoordNames( 181 | [ 182 | ['ud', Cx(Math.SQRT1_2)], 183 | ['du', Cx(-Math.SQRT1_2)], 184 | ], 185 | [Dimension.spin(), Dimension.spin()], 186 | ) 187 | 188 | expect(singlet.toBasisAll('spin', 'spin-y').toKetString('cartesian')).toEqual( 189 | '(0.71 +0.00i) |uy,dy⟩ + (-0.71 +0.00i) |dy,uy⟩', 190 | ) 191 | expect(singlet.toBasisAll('spin', 'spin-x').toKetString('cartesian')).toEqual( 192 | '(0.71 +0.00i) |ux,dx⟩ + (-0.71 +0.00i) |dx,ux⟩', 193 | ) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /tests/Circuit.test.ts: -------------------------------------------------------------------------------- 1 | import Circuit from '../src/Circuit' 2 | import './customMatchers' 3 | 4 | describe('Circuit', () => { 5 | it('creates a new quantum computing circuit', () => { 6 | const qc = Circuit.empty() 7 | const qc3 = qc 8 | .addQubit() 9 | .addQubit() 10 | .addQubit() 11 | 12 | expect(qc3.vector.toKetString('cartesian')).toBe('(1.00 +0.00i) |0,0,0⟩') 13 | expect(Circuit.qubits(3).vector).vectorCloseTo(qc3.vector) 14 | }) 15 | 16 | it('applied a few one-qubit gates', () => { 17 | const qc = Circuit.qubits(3) 18 | .X(0) 19 | .X(2) 20 | 21 | expect(qc.vector.toKetString('cartesian')).toBe('(1.00 +0.00i) |1,0,1⟩') 22 | }) 23 | 24 | it('applied CNOT', () => { 25 | const qc = Circuit.qubits(3) 26 | .H(1) 27 | .CNOT(1, 0) 28 | .X(2) 29 | 30 | expect(qc.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |0,0,1⟩ + (0.71 +0.00i) |1,1,1⟩') 31 | }) 32 | 33 | it('measurement', () => { 34 | const qc = Circuit.qubits(3) 35 | .H(1) 36 | .CNOT(1, 0) 37 | .X(2) 38 | 39 | const meas0 = qc.measureQubit(0) 40 | 41 | expect(meas0.length).toBe(2) 42 | expect(meas0[0].measured).toBe('0') 43 | expect(meas0[0].probability).toBeCloseTo(0.5) 44 | expect(meas0[0].newState.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |0,1⟩') 45 | expect(meas0[0].newState.qubitIds).toEqual([1, 2]) 46 | expect(meas0[1].measured).toBe('1') 47 | expect(meas0[1].probability).toBeCloseTo(0.5) 48 | expect(meas0[1].newState.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |1,1⟩') 49 | expect(meas0[1].newState.qubitIds).toEqual([1, 2]) 50 | 51 | const meas1 = qc.measureQubit(1) 52 | expect(meas1[0].probability).toBeCloseTo(0.5) 53 | expect(meas1[0].newState.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |0,1⟩') 54 | expect(meas1[0].newState.qubitIds).toEqual([0, 2]) 55 | expect(meas1[1].probability).toBeCloseTo(0.5) 56 | expect(meas1[1].newState.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |1,1⟩') 57 | expect(meas1[1].newState.qubitIds).toEqual([0, 2]) 58 | 59 | const meas2 = qc.measureQubit(2) 60 | expect(meas2[0].probability).toBeCloseTo(0) 61 | expect(meas2[0].newState.vector.toKetString('cartesian')).toBe('') 62 | expect(meas2[0].newState.qubitIds).toEqual([0, 1]) 63 | expect(meas2[1].probability).toBeCloseTo(1) 64 | expect(meas2[1].newState.vector.toKetString('cartesian')).toBe('(0.71 +0.00i) |0,0⟩ + (0.71 +0.00i) |1,1⟩') 65 | expect(meas2[1].newState.qubitIds).toEqual([0, 1]) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/Complex.test.ts: -------------------------------------------------------------------------------- 1 | import Complex, { Cx } from '../src/Complex' 2 | // import Complex from "../src/Complex" 3 | // import { VectorEntry } from "../src/Entry" 4 | 5 | // Coordinates testing 6 | describe('Complex', () => { 7 | it('should create a complex element from two numbers', () => { 8 | const complex = Cx(4, -4) 9 | expect(complex.isZero()).toBe(false) 10 | expect(complex.toString()).toEqual('(4.00 -4.00i)') 11 | }) 12 | 13 | it('should return the complex conjugate of a complex number', () => { 14 | const complex1 = Cx(4, -4) 15 | const complex2 = Cx(3, 0) 16 | const conj1 = complex1.conj() 17 | expect(conj1.isZero()).toBe(false) 18 | expect(conj1.toString()).toEqual('(4.00 +4.00i)') 19 | const conj2 = complex2.conj() 20 | expect(conj2.isZero()).toBe(false) 21 | expect(conj2.toString()).toEqual('(3.00 +0.00i)') 22 | }) 23 | 24 | it('should test if a Complex number is zero', () => { 25 | const complex1 = Cx(4) 26 | expect(complex1.isZero()).toBe(false) 27 | const complex2 = Cx(0, 1) 28 | expect(complex2.isZero()).toBe(false) 29 | const complex3 = Cx(0, 0) 30 | expect(complex3.isZero()).toBe(true) 31 | }) 32 | 33 | it('should give non-negative arg', () => { 34 | const z1 = Cx(1, -1) 35 | expect(z1.arg()).toEqual(1.75 * Math.PI) 36 | const z2 = Cx(0, -1) 37 | expect(z2.arg()).toEqual(1.5 * Math.PI) 38 | }) 39 | 40 | it('should give phase in Tau', () => { 41 | const z1 = Cx(1, -1) 42 | expect(z1.phiTau).toEqual(0.875) 43 | const z2 = Cx(-1) 44 | expect(z2.phiTau).toEqual(0.5) 45 | const z3 = Cx(0, -1) 46 | expect(z3.phiTau).toEqual(0.75) 47 | }) 48 | 49 | it('should add two complex numbers', () => { 50 | const complex1 = Cx(4, -1) 51 | const complex2 = Cx(2, 3) 52 | const result1 = complex1.add(complex2) 53 | const result2 = complex2.add(complex1) 54 | expect(result1).toEqual({ re: 6, im: 2 }) 55 | expect(result2).toEqual({ re: 6, im: 2 }) 56 | }) 57 | 58 | it('should substract two complex numbers', () => { 59 | const complex1 = Cx(4, -1) 60 | const complex2 = Cx(2, 3) 61 | const result1 = complex1.sub(complex2) 62 | const result2 = complex2.sub(complex1) 63 | expect(result1).toEqual({ re: 2, im: -4 }) 64 | expect(result2).toEqual({ re: -2, im: 4 }) 65 | }) 66 | 67 | it('should multiply two complex numbers', () => { 68 | const complex1 = Cx(3, 2) 69 | const complex2 = Cx(1, 7) 70 | const result1 = complex1.mul(complex2) 71 | const result2 = complex2.mul(complex1) 72 | expect(result1).toEqual({ re: -11, im: 23 }) 73 | expect(result2).toEqual({ re: -11, im: 23 }) 74 | }) 75 | 76 | it('should multiply two complex numbers using Gauss algorithm', () => { 77 | const complex1 = Cx(3, 2) 78 | const complex2 = Cx(1, 7) 79 | const result1 = complex1.mulGauss(complex2) 80 | const result2 = complex2.mulGauss(complex1) 81 | expect(result1).toEqual({ re: -11, im: 23 }) 82 | expect(result2).toEqual({ re: -11, im: 23 }) 83 | }) 84 | 85 | it('should normalize if this.r is not 0', () => { 86 | const complex1 = Cx(0, 0) 87 | const complex2 = Cx(3, 4) 88 | expect(() => complex1.normalize()).toThrowError('Cannot normalize a 0 length vector...') 89 | expect(complex2.normalize()).toEqual({ re: 0.6, im: 0.8 }) 90 | }) 91 | 92 | it('should test for equality with a similar complex number', () => { 93 | const complex1 = Cx(3, 4) 94 | const complex2 = Cx(3, 4) 95 | const complex3 = Cx(1, 0) 96 | expect(complex1.equal(complex2)).toBe(true) 97 | expect(complex1.equal(complex3)).toBe(false) 98 | }) 99 | 100 | it('should create a complex number from polar coordinates', () => { 101 | const r = 2 102 | const phi = 1 103 | const complex1 = Complex.fromPolar(r, phi) 104 | expect(complex1).toEqual({ im: 1.682941969615793, re: 1.0806046117362795 }) 105 | expect(complex1.r).toBe(2) 106 | expect(complex1.phi).toBe(1) 107 | }) 108 | 109 | it('should divide numbers, unless denominator i zero', () => { 110 | const z0 = Cx(0) 111 | const z1 = Cx(10, -5) 112 | const z2 = Cx(-3, 4) 113 | expect(z1.div(z2)).toEqual({ re: -2, im: -1 }) 114 | expect(() => z1.div(z0)).toThrowError('Cannot divide by 0. z1: (10.00 -5.00i) / z2: (0.00 +0.00i)') 115 | }) 116 | 117 | it('should divide numbers, unless denominator i zero', () => { 118 | const z0 = Cx(0) 119 | const z1 = Cx(10, -5) 120 | const z2 = Cx(-3, 4) 121 | expect(z1.div(z2)).toEqual({ re: -2, im: -1 }) 122 | expect(() => z1.div(z0)).toThrowError('Cannot divide by 0. z1: (10.00 -5.00i) / z2: (0.00 +0.00i)') 123 | }) 124 | 125 | it('should test if it a unit-length number', () => { 126 | expect(Cx(1).isNormal()).toBe(true) 127 | expect(Cx(0, -1).isNormal()).toBe(true) 128 | expect(Cx(1, -1).isNormal()).toBe(false) 129 | }) 130 | 131 | it('should print complex number', () => { 132 | const z1 = Cx(1, -1) 133 | expect(z1.toString()).toEqual('(1.00 -1.00i)') 134 | expect(z1.toString('cartesian')).toEqual('(1.00 -1.00i)') 135 | expect(z1.toString('cartesian', 1)).toEqual('(1.0 -1.0i)') 136 | expect(z1.toString('polar')).toEqual('1.41 exp(5.50i)') 137 | expect(z1.toString('polarTau', 4)).toEqual('1.4142 exp(0.8750τi)') 138 | }) 139 | 140 | it('should generate color for a complex number', () => { 141 | expect(Complex.fromPolar(1, 0).toColor()).toEqual('#ff0000') 142 | expect(Complex.fromPolar(1, 0.3333 * Math.PI).toColor()).toEqual('#ffff00') 143 | expect(Complex.fromPolar(1, 0.6666 * Math.PI).toColor()).toEqual('#00ff00') 144 | }) 145 | 146 | it('should generate a random complex number', () => { 147 | // it is effectively unlikely to get exactly 0 148 | // we don't test for actual randomness here 149 | expect(Complex.randomGaussian().isZero()).toBeFalsy() 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /tests/Dimension.test.ts: -------------------------------------------------------------------------------- 1 | import Dimension from '../src/Dimension' 2 | 3 | describe('Dimension', () => { 4 | const original = console.error 5 | 6 | beforeEach(() => { 7 | console.error = jest.fn() 8 | }) 9 | 10 | afterEach(() => { 11 | console.error = original 12 | }) 13 | 14 | it('should create dimensions from static methods', () => { 15 | const polarization = Dimension.polarization() 16 | const direction = Dimension.direction() 17 | const spin = Dimension.spin() 18 | expect(polarization).toMatchObject({ coordNames: ['H', 'V'], name: 'polarization', size: 2 }) 19 | expect(direction).toMatchObject({ coordNames: ['>', '^', '<', 'v'], name: 'direction', size: 4 }) 20 | expect(spin).toMatchObject({ coordNames: ['u', 'd'], name: 'spin', size: 2 }) 21 | }) 22 | 23 | it('should equate identical dimensions', () => { 24 | const polarization1 = Dimension.polarization() 25 | const polarization2 = Dimension.polarization() 26 | expect(polarization1.isEqual(polarization2)).toBeTruthy() 27 | }) 28 | 29 | it('should not equate different dimensions', () => { 30 | const polarization = Dimension.polarization() 31 | const direction = Dimension.direction() 32 | expect(polarization.isEqual(direction)).toBeFalsy() 33 | }) 34 | 35 | it('should forbid creating dimensions with a mismatched length of element and size', () => { 36 | const error1 = { 37 | name: 'polarization', 38 | size: 2, 39 | coordNames: ['H', 'V', 'WeaselDimension'], 40 | } 41 | const error2 = { 42 | name: 'polarization', 43 | size: 3, 44 | coordNames: ['H', 'V'], 45 | } 46 | expect(() => new Dimension(error1.name, error1.size, error1.coordNames)).toThrowError( 47 | 'Coordinates [H,V,WeaselDimension] array is of length 3, not 2.', 48 | ) 49 | expect(() => new Dimension(error2.name, error2.size, error2.coordNames)).toThrowError( 50 | 'Coordinates [H,V] array is of length 2, not 3.', 51 | ) 52 | }) 53 | 54 | it('should retrieve the index of a coordinate from the array of coordinates names', () => { 55 | const obj = { 56 | name: 'polarization', 57 | size: 3, 58 | coordNames: ['H', 'V', 'WeaselDimension'], 59 | } 60 | const dim = new Dimension(obj.name, obj.size, obj.coordNames) 61 | expect(dim.coordNameToIndex('WeaselDimension')).toBe(2) 62 | }) 63 | 64 | it('should error if provided a wrong coordinate name', () => { 65 | const obj = { 66 | name: 'polarization', 67 | size: 3, 68 | coordNames: ['H', 'V', 'WeaselDimension'], 69 | } 70 | const dim = new Dimension(obj.name, obj.size, obj.coordNames) 71 | expect(() => dim.coordNameToIndex('OctopusDimension')).toThrowError( 72 | 'OctopusDimension is not in [H,V,WeaselDimension]', 73 | ) 74 | }) 75 | 76 | it('should display errors related to dimension array size', () => { 77 | const obj1 = { 78 | name: 'polarization', 79 | size: 3, 80 | coordNames: ['H', 'V', 'WeaselDimension'], 81 | } 82 | const dim1 = new Dimension(obj1.name, obj1.size, obj1.coordNames) 83 | const dim2 = Dimension.spin() 84 | const dim3 = Dimension.polarization() 85 | expect(() => Dimension.checkDimensions([dim1], [dim2, dim3])).toThrowError('Dimensions array size mismatch...') 86 | }) 87 | 88 | it('should display errors related to dimension array order', () => { 89 | const obj1 = { 90 | name: 'polarization', 91 | size: 3, 92 | coordNames: ['H', 'V', 'WeaselDimension'], 93 | } 94 | const dim1 = new Dimension(obj1.name, obj1.size, obj1.coordNames) 95 | const dim2 = Dimension.spin() 96 | const dim3 = Dimension.polarization() 97 | expect(() => Dimension.checkDimensions([dim1, dim2], [dim2, dim3])).toThrowError( 98 | 'Dimensions array order mismatch...', 99 | ) 100 | }) 101 | 102 | it('creating many qubits at once', () => { 103 | const qubits1 = [Dimension.qubit(), Dimension.qubit(), Dimension.qubit()]; 104 | const qubits2 = Dimension.qubits(3); 105 | expect(() => Dimension.checkDimensions(qubits1, qubits2)).not.toThrow(); 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /tests/Elements.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import * as Elements from '../src/Elements' 3 | import './customMatchers' 4 | 5 | describe('Optical elements', () => { 6 | it('Sugar solution', () => { 7 | expect(Elements.sugarSolution().isCloseToUnitary()).toBe(true) 8 | expect(Elements.sugarSolution().isCloseToHermitian()).toBe(false) 9 | expect(Elements.incoherentLightOperator(Elements.sugarSolution())).operatorCloseToNumbers([ 10 | [Cx(1), Cx(0), Cx(0), Cx(0)], 11 | [Cx(0), Cx(1), Cx(0), Cx(0)], 12 | [Cx(0), Cx(0), Cx(1), Cx(0)], 13 | [Cx(0), Cx(0), Cx(0), Cx(1)], 14 | ]) 15 | }) 16 | 17 | it('Attenuator', () => { 18 | expect(Elements.attenuator().isCloseToUnitary()).toBe(false) 19 | expect(Elements.attenuator().isCloseToHermitian()).toBe(true) 20 | expect(Elements.incoherentLightOperator(Elements.attenuator())).operatorCloseToNumbers([ 21 | [Cx(0.5), Cx(0), Cx(0), Cx(0)], 22 | [Cx(0), Cx(0.5), Cx(0), Cx(0)], 23 | [Cx(0), Cx(0), Cx(0.5), Cx(0)], 24 | [Cx(0), Cx(0), Cx(0), Cx(0.5)], 25 | ]) 26 | }) 27 | 28 | it('Vacuum Jar (phase advancer)', () => { 29 | expect(Elements.vacuumJar().isCloseToUnitary()).toBe(true) 30 | expect(Elements.vacuumJar().isCloseToHermitian()).toBe(false) 31 | expect(Elements.incoherentLightOperator(Elements.vacuumJar())).operatorCloseToNumbers([ 32 | [Cx(1), Cx(0), Cx(0), Cx(0)], 33 | [Cx(0), Cx(1), Cx(0), Cx(0)], 34 | [Cx(0), Cx(0), Cx(1), Cx(0)], 35 | [Cx(0), Cx(0), Cx(0), Cx(1)], 36 | ]) 37 | }) 38 | 39 | it('Glass Slab (phase retarder)', () => { 40 | expect(Elements.glassSlab().isCloseToUnitary()).toBe(true) 41 | expect(Elements.glassSlab().isCloseToHermitian()).toBe(false) 42 | expect(Elements.incoherentLightOperator(Elements.glassSlab())).operatorCloseToNumbers([ 43 | [Cx(1), Cx(0), Cx(0), Cx(0)], 44 | [Cx(0), Cx(1), Cx(0), Cx(0)], 45 | [Cx(0), Cx(0), Cx(1), Cx(0)], 46 | [Cx(0), Cx(0), Cx(0), Cx(1)], 47 | ]) 48 | }) 49 | 50 | it('Mirror', () => { 51 | expect(Elements.mirror(0).isCloseToUnitary()).toBe(false) 52 | expect(Elements.mirror(0).isCloseToUnitaryOnSubspace()).toBe(true) 53 | expect(Elements.mirror(0).isCloseToHermitian()).toBe(true) 54 | 55 | expect(Elements.mirror(45).isCloseToUnitary()).toBe(true) 56 | expect(Elements.mirror(45).isCloseToHermitian()).toBe(true) 57 | 58 | expect(Elements.mirror(90).isCloseToUnitary()).toBe(false) 59 | expect(Elements.mirror(90).isCloseToUnitaryOnSubspace()).toBe(true) 60 | expect(Elements.mirror(90).isCloseToHermitian()).toBe(true) 61 | 62 | expect(Elements.mirror(135).isCloseToUnitary()).toBe(true) 63 | expect(Elements.mirror(135).isCloseToHermitian()).toBe(true) 64 | }) 65 | 66 | it('Beam splitter', () => { 67 | expect(Elements.beamSplitter(0).isCloseToUnitary()).toBe(false) 68 | expect(Elements.beamSplitter(0).isCloseToUnitaryOnSubspace()).toBe(true) 69 | expect(Elements.beamSplitter(0).isCloseToHermitian()).toBe(false) 70 | 71 | expect(Elements.beamSplitter(45).isCloseToUnitary()).toBe(true) 72 | expect(Elements.beamSplitter(45).isCloseToHermitian()).toBe(false) 73 | 74 | expect(Elements.beamSplitter(90).isCloseToUnitary()).toBe(false) 75 | expect(Elements.beamSplitter(90).isCloseToUnitaryOnSubspace()).toBe(true) 76 | expect(Elements.beamSplitter(90).isCloseToHermitian()).toBe(false) 77 | 78 | expect(Elements.beamSplitter(135).isCloseToUnitary()).toBe(true) 79 | expect(Elements.beamSplitter(135).isCloseToHermitian()).toBe(false) 80 | }) 81 | 82 | it('Corner cube', () => { 83 | expect(Elements.cornerCube().isCloseToUnitary()).toBe(true) 84 | expect(Elements.cornerCube().isCloseToHermitian()).toBe(true) 85 | expect(Elements.incoherentLightOperator(Elements.cornerCube())).operatorCloseToNumbers([ 86 | [Cx(0), Cx(0), Cx(1), Cx(0)], 87 | [Cx(0), Cx(0), Cx(0), Cx(1)], 88 | [Cx(1), Cx(0), Cx(0), Cx(0)], 89 | [Cx(0), Cx(1), Cx(0), Cx(0)], 90 | ]) 91 | }) 92 | 93 | it('Polarizing beam splitter', () => { 94 | expect(Elements.polarizingBeamsplitter(0).isCloseToUnitary()).toBe(true) 95 | expect(Elements.polarizingBeamsplitter(0).isCloseToHermitian()).toBe(true) 96 | expect(Elements.incoherentLightOperator(Elements.polarizingBeamsplitter(0))).operatorCloseToNumbers([ 97 | [Cx(0.5), Cx(0.5), Cx(0), Cx(0)], 98 | [Cx(0.5), Cx(0.5), Cx(0), Cx(0)], 99 | [Cx(0), Cx(0), Cx(0.5), Cx(0.5)], 100 | [Cx(0), Cx(0), Cx(0.5), Cx(0.5)], 101 | ]) 102 | 103 | expect(Elements.polarizingBeamsplitter(90).isCloseToUnitary()).toBe(true) 104 | expect(Elements.polarizingBeamsplitter(90).isCloseToHermitian()).toBe(true) 105 | expect(Elements.incoherentLightOperator(Elements.polarizingBeamsplitter(90))).operatorCloseToNumbers([ 106 | [Cx(0.5), Cx(0), Cx(0), Cx(0.5)], 107 | [Cx(0), Cx(0.5), Cx(0.5), Cx(0)], 108 | [Cx(0), Cx(0.5), Cx(0.5), Cx(0)], 109 | [Cx(0.5), Cx(0), Cx(0), Cx(0.5)], 110 | ]) 111 | }) 112 | 113 | it('Faraday rotator', () => { 114 | expect(Elements.faradayRotator(0).isCloseToUnitary()).toBe(false) 115 | expect(Elements.faradayRotator(0).isCloseToUnitaryOnSubspace()).toBe(true) 116 | expect(Elements.faradayRotator(0).isCloseToHermitian()).toBe(false) 117 | 118 | expect(Elements.faradayRotator(90).isCloseToUnitary()).toBe(false) 119 | expect(Elements.faradayRotator(90).isCloseToUnitaryOnSubspace()).toBe(true) 120 | expect(Elements.faradayRotator(90).isCloseToHermitian()).toBe(false) 121 | 122 | expect(Elements.faradayRotator(180).isCloseToUnitary()).toBe(false) 123 | expect(Elements.faradayRotator(180).isCloseToUnitaryOnSubspace()).toBe(true) 124 | expect(Elements.faradayRotator(180).isCloseToHermitian()).toBe(false) 125 | 126 | expect(Elements.faradayRotator(270).isCloseToUnitary()).toBe(false) 127 | expect(Elements.faradayRotator(270).isCloseToUnitaryOnSubspace()).toBe(true) 128 | expect(Elements.faradayRotator(270).isCloseToHermitian()).toBe(false) 129 | }) 130 | 131 | it('Polarizers', () => { 132 | expect(Elements.polarizer(0).isCloseToProjection()).toBe(true) 133 | expect(Elements.polarizer(45).isCloseToProjection()).toBe(true) 134 | expect(Elements.polarizer(90).isCloseToProjection()).toBe(true) 135 | expect(Elements.polarizer(135).isCloseToProjection()).toBe(true) 136 | }) 137 | 138 | it('Quarter wave plates', () => { 139 | expect(Elements.phasePlate(0, 0.25).isCloseToUnitaryOnSubspace()).toBe(true) 140 | expect(Elements.phasePlate(45, 0.25).isCloseToUnitaryOnSubspace()).toBe(true) 141 | expect(Elements.phasePlate(90, 0.25).isCloseToUnitaryOnSubspace()).toBe(true) 142 | expect(Elements.phasePlate(135, 0.25).isCloseToUnitaryOnSubspace()).toBe(true) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /tests/Entanglement.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import Vector from '../src/Vector' 4 | import Entanglement from '../src/Entanglement' 5 | 6 | describe('Entanglement', () => { 7 | 8 | it('Renyi 2 product state', () => { 9 | const vec0 = Vector.fromSparseCoordNames( 10 | [ 11 | ['uu', Cx(1, 0)], 12 | ['du', Cx(0, 1)], 13 | ], 14 | [Dimension.spin(), Dimension.spin()], 15 | ).normalize() 16 | expect(Entanglement.renyi2(vec0, [0])).toBeCloseTo(0) 17 | }) 18 | 19 | it('Renyi 2 entangled states', () => { 20 | const vec1 = Vector.fromSparseCoordNames( 21 | [ 22 | ['uu', Cx(1, 0)], 23 | ['dd', Cx(0, 1)], 24 | ], 25 | [Dimension.spin(), Dimension.spin()], 26 | ).normalize() 27 | expect(Entanglement.renyi2(vec1, [0])).toBeCloseTo(1) 28 | 29 | const vec2 = Vector.fromSparseCoordNames( 30 | [ 31 | ['>3', Cx(1, 0)], 32 | ['^0', Cx(0, 1)], 33 | ['<1', Cx(0, -1)], 34 | ], 35 | [Dimension.direction(), Dimension.position(5)], 36 | ).normalize() 37 | expect(Entanglement.renyi2(vec2, [0])).toBeCloseTo(Math.log2(3)) 38 | 39 | const vec3 = Vector.fromSparseCoordNames( 40 | [ 41 | ['>3', Cx(1, 0)], 42 | ['>0', Cx(0, 1)], 43 | ['<1', Cx(-1, 0)], 44 | ['<2', Cx(0, -1)], 45 | ], 46 | [Dimension.direction(), Dimension.position(5)], 47 | ).normalize() 48 | expect(Entanglement.renyi2(vec3, [0])).toBeCloseTo(1) 49 | 50 | const vec4 = Vector.fromSparseCoordNames( 51 | [ 52 | ['>3u', Cx(1, 0)], 53 | ['>0u', Cx(0, 1)], 54 | ['<1d', Cx(-1, 0)], 55 | ['<2d', Cx(0, -1)], 56 | ], 57 | [Dimension.direction(), Dimension.position(5), Dimension.spin()], 58 | ).normalize() 59 | expect(Entanglement.renyi2(vec4, [0, 2])).toBeCloseTo(1) 60 | 61 | const vec5 = Vector.fromSparseCoordNames( 62 | [ 63 | ['>3u', Cx(1, 0)], 64 | ['>0d', Cx(0, 1)], 65 | ['<1u', Cx(-1, 0)], 66 | ['<2d', Cx(0, -1)], 67 | ], 68 | [Dimension.direction(), Dimension.position(5), Dimension.spin()], 69 | ).normalize() 70 | expect(Entanglement.renyi2(vec5, [0, 2])).toBeCloseTo(2) 71 | }) 72 | 73 | }) -------------------------------------------------------------------------------- /tests/Gates.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import * as Gates from '../src/Gates' 3 | import './customMatchers' 4 | 5 | describe('X, Y, Z Gate', () => { 6 | const X = Gates.X() 7 | const Y = Gates.Y() 8 | const Z = Gates.Z() 9 | 10 | it('X should create 0<->1', () => { 11 | expect(X.isCloseToUnitary()).toBe(true) 12 | expect(X).operatorCloseToNumbers([ 13 | [Cx(0), Cx(1)], 14 | [Cx(1), Cx(0)], 15 | ]) 16 | expect(X.mulOp(X).isCloseToIdentity()).toBe(true) 17 | }) 18 | 19 | it('Y', () => { 20 | expect(Y.isCloseToUnitary()).toBe(true) 21 | expect(Y).operatorCloseToNumbers([ 22 | [Cx(0), Cx(0, -1)], 23 | [Cx(0, 1), Cx(0)], 24 | ]) 25 | expect(Y.mulOp(Y).isCloseToIdentity()).toBe(true) 26 | }) 27 | 28 | it('Z', () => { 29 | expect(Z.isCloseToUnitary()).toBe(true) 30 | expect(Z).operatorCloseToNumbers([ 31 | [Cx(1), Cx(0)], 32 | [Cx(0), Cx(-1)], 33 | ]) 34 | expect(Z.mulOp(Z).isCloseToIdentity()).toBe(true) 35 | }) 36 | 37 | it('X * Y = i Z', () => { 38 | const lhs = X.mulOp(Y) 39 | const rhs = Z.mulConstant(Cx(0, 1)) 40 | expect(lhs).operatorCloseToNumbers(rhs.toDense()) 41 | }) 42 | }) 43 | 44 | describe('All gates', () => { 45 | it('identity to be identity', () => { 46 | expect(Gates.I().isCloseToIdentity()).toBe(true) 47 | }) 48 | 49 | it('all 1-qubit gates are unitary', () => { 50 | expect(Gates.I().isCloseToUnitary()).toBe(true) 51 | expect(Gates.X().isCloseToUnitary()).toBe(true) 52 | expect(Gates.Y().isCloseToUnitary()).toBe(true) 53 | expect(Gates.Z().isCloseToUnitary()).toBe(true) 54 | expect(Gates.H().isCloseToUnitary()).toBe(true) 55 | expect(Gates.S().isCloseToUnitary()).toBe(true) 56 | expect(Gates.T().isCloseToUnitary()).toBe(true) 57 | }) 58 | 59 | it('some 1-qubit gates are Hermitian', () => { 60 | expect(Gates.I().isCloseToHermitian()).toBe(true) 61 | expect(Gates.X().isCloseToHermitian()).toBe(true) 62 | expect(Gates.Y().isCloseToHermitian()).toBe(true) 63 | expect(Gates.Z().isCloseToHermitian()).toBe(true) 64 | expect(Gates.H().isCloseToHermitian()).toBe(true) 65 | expect(Gates.S().isCloseToHermitian()).toBe(false) 66 | expect(Gates.T().isCloseToHermitian()).toBe(false) 67 | }) 68 | 69 | it('all 2-qubit gates are unitary', () => { 70 | expect(Gates.CX().isCloseToUnitary()).toBe(true) 71 | expect(Gates.CZ().isCloseToUnitary()).toBe(true) 72 | expect(Gates.Swap().isCloseToUnitary()).toBe(true) 73 | }) 74 | 75 | it('all 2-qubit gates are Hermitian', () => { 76 | expect(Gates.CX().isCloseToHermitian()).toBe(true) 77 | expect(Gates.CZ().isCloseToHermitian()).toBe(true) 78 | expect(Gates.Swap().isCloseToHermitian()).toBe(true) 79 | }) 80 | 81 | it('all 3-qubit gates are unitary', () => { 82 | expect(Gates.CCX().isCloseToUnitary()).toBe(true) 83 | expect(Gates.CSwap().isCloseToUnitary()).toBe(true) 84 | }) 85 | 86 | it('all 3-qubit gates are Hermitian', () => { 87 | expect(Gates.CCX().isCloseToHermitian()).toBe(true) 88 | expect(Gates.CSwap().isCloseToHermitian()).toBe(true) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/IncoherentLight.test.ts: -------------------------------------------------------------------------------- 1 | import Dimension from '../src/Dimension' 2 | import IncoherentLight from '../src/IncoherentLight' 3 | import * as Elements from '../src/Elements' 4 | import { IXYOperator } from '../src/interfaces' 5 | import './customMatchers' 6 | 7 | describe('IncoherentLight', () => { 8 | it('creates some space', () => { 9 | const space = IncoherentLight.emptySpace(7, 5) 10 | 11 | expect(space.dimX).toEqual(Dimension.position(7, 'x')) 12 | expect(space.dimY).toEqual(Dimension.position(5, 'y')) 13 | 14 | space.addIntensityFromIndicator(0, 2, '>') 15 | 16 | expect(space.totalIntensity).toBeCloseTo(1) 17 | expect(space.ketString()).toBe('1.00 |0,2,>⟩') 18 | }) 19 | 20 | it('propagates a beam', () => { 21 | const space = IncoherentLight.emptySpace(3, 5).addIntensityFromIndicator(0, 2, '>') 22 | 23 | expect(space.ketString()).toBe('1.00 |0,2,>⟩') 24 | space.propagateBeam() 25 | expect(space.ketString()).toBe('1.00 |1,2,>⟩') 26 | space.propagateBeam() 27 | expect(space.ketString()).toBe('1.00 |2,2,>⟩') 28 | space.propagateBeam() 29 | expect(space.ketString()).toBe('') 30 | }) 31 | 32 | it('beam interaction with elements', () => { 33 | const space = IncoherentLight.emptySpace(7, 6).addIntensityFromIndicator(0, 2, '>') 34 | 35 | const operations: IXYOperator[] = IncoherentLight.opsWithPosMakeIncoherent([ 36 | { x: 1, y: 5, op: Elements.sugarSolution(0.125) }, 37 | { x: 1, y: 2, op: Elements.mirror(135) }, 38 | { x: 1, y: 4, op: Elements.attenuator() }, 39 | ]) 40 | 41 | expect(space.ketString()).toBe('1.00 |0,2,>⟩') 42 | space.propagateBeam().interact(operations) 43 | expect(space.ketString()).toBe('1.00 |1,2,v⟩') 44 | space.propagateBeam().interact(operations) 45 | expect(space.ketString()).toBe('1.00 |1,3,v⟩') 46 | space.propagateBeam().interact(operations) 47 | expect(space.ketString()).toBe('0.50 |1,4,v⟩') 48 | space.propagateBeam().interact(operations) 49 | expect(space.ketString()).toBe('0.50 |1,5,v⟩') 50 | }) 51 | 52 | it('no interference', () => { 53 | // Sugar solutions and mirrors 54 | // Let's start horizontal 55 | // >.\..\. 56 | // ..\VV\. 57 | // ....... 58 | 59 | const space = IncoherentLight.emptySpace(8, 3).addIntensityFromIndicator(0, 0, '>') 60 | const operations: IXYOperator[] = IncoherentLight.opsWithPosMakeIncoherent([ 61 | { x: 2, y: 0, op: Elements.beamSplitter(135) }, 62 | { x: 5, y: 0, op: Elements.mirror(135) }, 63 | { x: 2, y: 1, op: Elements.mirror(135) }, 64 | { x: 3, y: 1, op: Elements.vacuumJar() }, 65 | { x: 4, y: 1, op: Elements.vacuumJar() }, 66 | { x: 5, y: 1, op: Elements.beamSplitter(135) }, 67 | ]) 68 | 69 | expect(space.ketString()).toBe('1.00 |0,0,>⟩') 70 | space.propagateBeam().interact(operations) 71 | expect(space.ketString()).toBe('1.00 |1,0,>⟩') 72 | space.propagateBeam().interact(operations) 73 | expect(space.ketString()).toBe('0.50 |2,0,>⟩ + 0.50 |2,0,v⟩') 74 | space.propagateBeam().interact(operations) 75 | expect(space.ketString()).toBe('0.50 |2,1,>⟩ + 0.50 |3,0,>⟩') 76 | space.propagateBeam().interact(operations) 77 | expect(space.ketString()).toBe('0.50 |3,1,>⟩ + 0.50 |4,0,>⟩') 78 | space.propagateBeam().interact(operations) 79 | expect(space.ketString()).toBe('0.50 |4,1,>⟩ + 0.50 |5,0,v⟩') 80 | space.propagateBeam().interact(operations) 81 | expect(space.ketString()).toBe('0.50 |5,1,>⟩ + 0.50 |5,1,v⟩') 82 | }) 83 | 84 | it('polarizing beam splitters', () => { 85 | // Sugar solutions and PBS 86 | // Let's start horizontal 87 | // >.S.\.. 88 | // ..../.. 89 | // ....... 90 | 91 | const space = IncoherentLight.emptySpace(8, 3).addIntensityFromIndicator(0, 0, '>') 92 | const operations: IXYOperator[] = IncoherentLight.opsWithPosMakeIncoherent([ 93 | { x: 2, y: 0, op: Elements.sugarSolution() }, 94 | { x: 4, y: 0, op: Elements.polarizingBeamsplitter(90) }, 95 | { x: 4, y: 1, op: Elements.polarizingBeamsplitter(0) }, 96 | { x: 5, y: 0, op: Elements.polarizingBeamsplitter(0) }, 97 | ]) 98 | 99 | expect(space.ketString()).toBe('1.00 |0,0,>⟩') 100 | space.propagateBeam() 101 | space.propagateBeam() 102 | space.interact(operations) 103 | expect(space.ketString()).toBe('1.00 |2,0,>⟩') 104 | space.propagateBeam() 105 | space.propagateBeam() 106 | space.interact(operations) 107 | expect(space.ketString()).toBe('0.50 |4,0,>⟩ + 0.50 |4,0,v⟩') 108 | space.propagateBeam() 109 | space.interact(operations) 110 | expect(space.ketString()).toBe('0.25 |4,1,<⟩ + 0.25 |4,1,v⟩ + 0.25 |5,0,>⟩ + 0.25 |5,0,^⟩') 111 | }) 112 | 113 | it('attenuation', () => { 114 | // 50% absorption filters 115 | // >.\AA.. 116 | // ....... 117 | // ..A.... 118 | // ....... 119 | 120 | const space = IncoherentLight.emptySpace(7, 4).addIntensityFromIndicator(0, 0, '>') 121 | const operations: IXYOperator[] = IncoherentLight.opsWithPosMakeIncoherent([ 122 | { x: 2, y: 0, op: Elements.beamSplitter(135) }, 123 | { x: 3, y: 0, op: Elements.attenuator() }, 124 | { x: 4, y: 0, op: Elements.attenuator() }, 125 | { x: 2, y: 2, op: Elements.attenuator() }, 126 | ]) 127 | expect(space.ketString()).toBe('1.00 |0,0,>⟩') 128 | space.propagateBeam().interact(operations) 129 | expect(space.ketString()).toBe('1.00 |1,0,>⟩') 130 | space.propagateBeam().interact(operations) 131 | expect(space.ketString()).toBe('0.50 |2,0,>⟩ + 0.50 |2,0,v⟩') 132 | space.propagateBeam().interact(operations) 133 | expect(space.ketString()).toBe('0.50 |2,1,v⟩ + 0.25 |3,0,>⟩') 134 | space.propagateBeam().interact(operations) 135 | expect(space.ketString(3)).toBe('0.250 |2,2,v⟩ + 0.125 |4,0,>⟩') 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /tests/Measurement.test.ts: -------------------------------------------------------------------------------- 1 | import Measurement, { WeightedProjection } from '../src/Measurement' 2 | import Vector from '../src/Vector' 3 | import Dimension from '../src/Dimension' 4 | import { Cx } from '../src/Complex' 5 | import './customMatchers' 6 | import Operator from '../src/Operator' 7 | 8 | describe('Measurement', () => { 9 | it('the simplest measurement', () => { 10 | const vector = Vector.fromSparseCoordNames( 11 | [ 12 | ['0', Cx(1)], 13 | ['1', Cx(1)], 14 | ], 15 | [Dimension.qubit()], 16 | ).normalize() 17 | const m = new Measurement([{ name: [], vector }]) 18 | expect(m.states.length).toBe(1) 19 | expect(m.toString()).toBe('100.0% [] 0.71 exp(0.00τi) |0⟩ + 0.71 exp(0.00τi) |1⟩') 20 | const projections = [ 21 | { name: ['zero'], vector: Vector.indicator([Dimension.qubit()], '0') }, 22 | { name: ['one'], vector: Vector.indicator([Dimension.qubit()], '1') }, 23 | ] 24 | const newM = m.projectiveMeasurement([0], projections) 25 | expect(newM.toString()).toBe(['50.0% [zero] 1.00 exp(0.00τi) |⟩', '50.0% [one] 1.00 exp(0.00τi) |⟩'].join('\n')) 26 | }) 27 | 28 | it('two particles', () => { 29 | const vector = Vector.fromSparseCoordNames( 30 | [ 31 | ['01', Cx(1)], 32 | ['10', Cx(-1)], 33 | ], 34 | [Dimension.qubit(), Dimension.qubit()], 35 | ).normalize() 36 | const m = new Measurement([{ name: [], vector }]) 37 | const projections = [ 38 | { name: ['zero'], vector: Vector.indicator([Dimension.qubit()], '0') }, 39 | { name: ['one'], vector: Vector.indicator([Dimension.qubit()], '1') }, 40 | ] 41 | const measFirst = m.projectiveMeasurement([0], projections) 42 | expect(measFirst.toString()).toBe( 43 | ['50.0% [zero] 1.00 exp(0.00τi) |1⟩', '50.0% [one] 1.00 exp(0.50τi) |0⟩'].join('\n'), 44 | ) 45 | 46 | const measLast = m.projectiveMeasurement([1], projections) 47 | expect(measLast.toString()).toBe( 48 | ['50.0% [zero] 1.00 exp(0.50τi) |1⟩', '50.0% [one] 1.00 exp(0.00τi) |0⟩'].join('\n'), 49 | ) 50 | }) 51 | 52 | it('non-complete projection', () => { 53 | const vector = Vector.fromSparseCoordNames( 54 | [ 55 | ['01H', Cx(1)], 56 | ['10V', Cx(-1)], 57 | ], 58 | [Dimension.qubit(), Dimension.qubit(), Dimension.polarization()], 59 | ).normalize() 60 | const m = new Measurement([{ name: [], vector }]) 61 | const projections = [ 62 | { name: ['0', '0'], vector: Vector.indicator([Dimension.qubit(), Dimension.qubit()], ['0', '0']) }, 63 | { name: ['0', '1'], vector: Vector.indicator([Dimension.qubit(), Dimension.qubit()], ['0', '1']) }, 64 | ] 65 | const measured = m.projectiveMeasurement([0, 1], projections) 66 | expect(measured.toString()).toBe( 67 | ['50.0% [] 1.00 exp(0.50τi) |1,0,V⟩', '50.0% [0&1] 1.00 exp(0.00τi) |H⟩'].join('\n'), 68 | ) 69 | }) 70 | 71 | it('probabilistic measurement', () => { 72 | const vector = Vector.fromSparseCoordNames( 73 | [ 74 | ['01H', Cx(1)], 75 | ['10V', Cx(-1)], 76 | ], 77 | [Dimension.qubit(), Dimension.qubit(), Dimension.polarization()], 78 | ).normalize() 79 | const m = new Measurement([{ name: [], vector }]) 80 | const projections = [ 81 | { name: ['H'], vector: Vector.indicator([Dimension.polarization()], 'H').mulByReal(Math.SQRT1_2) }, 82 | { name: ['V'], vector: Vector.indicator([Dimension.polarization()], 'V').mulByReal(Math.SQRT1_2) }, 83 | ] 84 | const measured = m.projectiveMeasurement([2], projections) 85 | expect(measured.toString()).toBe( 86 | [ 87 | '50.0% [] 0.71 exp(0.00τi) |0,1,H⟩ + 0.71 exp(0.50τi) |1,0,V⟩', 88 | '25.0% [H] 1.00 exp(0.00τi) |0,1⟩', 89 | '25.0% [V] 1.00 exp(0.50τi) |1,0⟩', 90 | ].join('\n'), 91 | ) 92 | }) 93 | 94 | it('more advanced measurement', () => { 95 | const vector = Vector.fromSparseCoordNames( 96 | [ 97 | ['dH0', Cx(0.5)], 98 | ['dV1', Cx(-0.5)], 99 | ['dV2', Cx(0, 0.5)], 100 | ['uV2', Cx(0, -0.5)], 101 | ], 102 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 103 | ) 104 | const m = new Measurement([{ name: [], vector }]) 105 | expect(m.states.length).toBe(1) 106 | expect(m.toString()).toBe( 107 | // eslint-disable-next-line max-len 108 | '100.0% [] 0.50 exp(0.75τi) |u,V,2⟩ + 0.50 exp(0.00τi) |d,H,0⟩ + 0.50 exp(0.50τi) |d,V,1⟩ + 0.50 exp(0.25τi) |d,V,2⟩', 109 | ) 110 | const projections = [ 111 | { name: ['H'], vector: Vector.indicator([Dimension.polarization()], 'H') }, 112 | { name: ['V'], vector: Vector.indicator([Dimension.polarization()], 'V') }, 113 | ] 114 | const newM = m.projectiveMeasurement([1], projections) 115 | expect(newM.states.length).toBe(2) 116 | expect(newM.states[0].name).toEqual(['H']) 117 | expect(newM.states[0].vector.toKetString('cartesian')).toEqual('(0.50 +0.00i) |d,0⟩') 118 | expect(newM.states[1].name).toEqual(['V']) 119 | expect(newM.states[1].vector.toKetString('cartesian')).toEqual( 120 | '(0.00 -0.50i) |u,2⟩ + (-0.50 +0.00i) |d,1⟩ + (0.00 +0.50i) |d,2⟩', 121 | ) 122 | expect(newM.toString()).toBe( 123 | [ 124 | '25.0% [H] 1.00 exp(0.00τi) |d,0⟩', 125 | '75.0% [V] 0.58 exp(0.75τi) |u,2⟩ + 0.58 exp(0.50τi) |d,1⟩ + 0.58 exp(0.25τi) |d,2⟩', 126 | ].join('\n'), 127 | ) 128 | }) 129 | 130 | it('cascade partial measurement', () => { 131 | const vector = Vector.fromSparseCoordNames( 132 | [ 133 | ['dH0', Cx(0.5)], 134 | ['dV1', Cx(-0.5)], 135 | ['dV2', Cx(0, 0.5)], 136 | ['uV2', Cx(0, -0.5)], 137 | ], 138 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 139 | ) 140 | const m = new Measurement([{ name: [], vector }]) 141 | const projectionsOne = [ 142 | { name: ['0'], vector: Vector.indicator([Dimension.position(3)], '0') }, 143 | { name: ['2'], vector: Vector.indicator([Dimension.position(3)], '2') }, 144 | ] 145 | const projectionsTwo = [ 146 | { name: ['H'], vector: Vector.indicator([Dimension.polarization()], 'H') }, 147 | { name: ['V'], vector: Vector.indicator([Dimension.polarization()], 'V').mulByReal(Math.SQRT1_2) }, 148 | ] 149 | const stepOne = m.projectiveMeasurement([2], projectionsOne) 150 | expect(stepOne.toString()).toBe( 151 | [ 152 | '25.0% [] 1.00 exp(0.50τi) |d,V,1⟩', 153 | '25.0% [0] 1.00 exp(0.00τi) |d,H⟩', 154 | '50.0% [2] 0.71 exp(0.75τi) |u,V⟩ + 0.71 exp(0.25τi) |d,V⟩', 155 | ].join('\n'), 156 | ) 157 | // crucial remark: as the number of dimensions changes, we need to apply operations in reverse order, 158 | // i.e. starting from the last dimension/particle 159 | const stepTwo = stepOne.projectiveMeasurement([1], projectionsTwo) 160 | expect(stepTwo.toString()).toBe( 161 | [ 162 | '12.5% [] 1.00 exp(0.50τi) |d,V,1⟩', 163 | '25.0% [2] 0.71 exp(0.75τi) |u,V⟩ + 0.71 exp(0.25τi) |d,V⟩', 164 | '12.5% [V] 1.00 exp(0.50τi) |d,1⟩', 165 | '25.0% [0&H] 1.00 exp(0.00τi) |d⟩', 166 | '25.0% [2&V] 0.71 exp(0.75τi) |u⟩ + 0.71 exp(0.25τi) |d⟩', 167 | ].join('\n'), 168 | ) 169 | }) 170 | 171 | it('Weighted projection checks if it is a projection', () => { 172 | const opProj = Operator.fromSparseCoordNames([ 173 | ['H', 'H', Cx(1)], 174 | ], [Dimension.polarization()]) 175 | const opNotProj = Operator.fromSparseCoordNames([ 176 | ['V', 'H', Cx(0, 1)], 177 | ['H', 'V', Cx(0, -1)], 178 | ], [Dimension.polarization()]) 179 | expect(() => WeightedProjection.new(['x'], opProj)).not.toThrowError() 180 | expect(() => WeightedProjection.new(['x'], opNotProj)).toThrowError('WeightedProjection x is not a projection.') 181 | }) 182 | 183 | it('POVM measurement all', () => { 184 | const vector = Vector.fromSparseCoordNames( 185 | [ 186 | ['01H', Cx(1)], 187 | ['10V', Cx(-1)], 188 | ], 189 | [Dimension.qubit(), Dimension.qubit(), Dimension.polarization()], 190 | ).normalize() 191 | const m = new Measurement([{ name: [], vector }]) 192 | const povms = [ 193 | WeightedProjection.new(['H'], Operator.indicator([Dimension.polarization()], 'H')), 194 | WeightedProjection.new(['V'], Operator.indicator([Dimension.polarization()], 'V')), 195 | ] 196 | const measured = m.projectiveMeasurement([2], [], povms) 197 | expect(measured.toString()).toBe( 198 | ['50.0% [H] 1.00 exp(0.00τi) |0,1,H⟩', '50.0% [V] 1.00 exp(0.50τi) |1,0,V⟩'].join('\n'), 199 | ) 200 | }) 201 | it('POVM measurement part', () => { 202 | const vector = Vector.fromSparseCoordNames( 203 | [ 204 | ['01H', Cx(1)], 205 | ['10V', Cx(-1)], 206 | ], 207 | [Dimension.qubit(), Dimension.qubit(), Dimension.polarization()], 208 | ).normalize() 209 | const m = new Measurement([{ name: [], vector }]) 210 | const povms = [ 211 | WeightedProjection.new(['H'], Operator.indicator([Dimension.polarization()], 'H'), 0.5), 212 | WeightedProjection.new(['V'], Operator.indicator([Dimension.polarization()], 'V'), 1.0), 213 | ] 214 | const measured = m.projectiveMeasurement([2], [], povms) 215 | expect(measured.toString()).toBe( 216 | [ 217 | '25.0% [] 1.00 exp(0.00τi) |0,1,H⟩', 218 | '25.0% [H] 1.00 exp(0.00τi) |0,1,H⟩', 219 | '50.0% [V] 1.00 exp(0.50τi) |1,0,V⟩', 220 | ].join('\n'), 221 | ) 222 | }) 223 | 224 | it('Projective + POVM measurement', () => { 225 | const vector = Vector.fromSparseCoordNames( 226 | [ 227 | ['dH0', Cx(0.5)], 228 | ['dV1', Cx(-0.5)], 229 | ['dV2', Cx(0, 0.5)], 230 | ['uV2', Cx(0, -0.5)], 231 | ], 232 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 233 | ) 234 | const m = new Measurement([{ name: [], vector }]) 235 | const projections = [ 236 | { name: ['0'], vector: Vector.indicator([Dimension.position(3)], '0') }, 237 | ] 238 | const povms = [ 239 | WeightedProjection.new(['1nondest'], Operator.indicator([Dimension.position(3)], '1'), 0.5), 240 | WeightedProjection.new(['2nondest'], Operator.indicator([Dimension.position(3)], '2'), 0.25), 241 | ] 242 | const measured = m.projectiveMeasurement([2], projections, povms) 243 | expect(measured.toString()).toBe( 244 | [ 245 | '50.0% [] 0.61 exp(0.75τi) |u,V,2⟩ + 0.50 exp(0.50τi) |d,V,1⟩ + 0.61 exp(0.25τi) |d,V,2⟩', 246 | '25.0% [0] 1.00 exp(0.00τi) |d,H⟩', 247 | '12.5% [1nondest] 1.00 exp(0.50τi) |d,V,1⟩', 248 | '12.5% [2nondest] 0.71 exp(0.75τi) |u,V,2⟩ + 0.71 exp(0.25τi) |d,V,2⟩', 249 | ].join('\n'), 250 | ) 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /tests/Operator.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import Vector from '../src/Vector' 4 | import Operator from '../src/Operator' 5 | import * as Elements from '../src/Elements' 6 | import './customMatchers' 7 | 8 | describe('Sparse Complex Operator', () => { 9 | const original = console.error 10 | 11 | beforeEach(() => { 12 | console.error = jest.fn() 13 | }) 14 | 15 | afterEach(() => { 16 | console.error = original 17 | }) 18 | 19 | it('creates identity', () => { 20 | const idPol = Operator.identity([Dimension.polarization()]) 21 | expect(idPol.toString('cartesian', 2, ' + ', false)).toEqual('(1.00 +0.00i) |H⟩⟨H| + (1.00 +0.00i) |V⟩⟨V|') 22 | }) 23 | 24 | it('creates from array', () => { 25 | const spinY = Operator.fromArray( 26 | [ 27 | [Cx(0), Cx(0, -1)], 28 | [Cx(0, 1), Cx(0)], 29 | ], 30 | [Dimension.spin()], 31 | ) 32 | expect(spinY.toString('cartesian', 2, ' + ', false)).toEqual('(0.00 -1.00i) |u⟩⟨d| + (0.00 +1.00i) |d⟩⟨u|') 33 | }) 34 | 35 | it('exports to dense', () => { 36 | const array = [ 37 | [Cx(0), Cx(0, -1)], 38 | [Cx(0, 1), Cx(0)], 39 | ] 40 | const spinY = Operator.fromArray(array, [Dimension.spin()]) 41 | expect(spinY.toDense()).toEqual(array) 42 | }) 43 | 44 | it('creates from sparse', () => { 45 | const opX = Operator.fromSparseCoordNames( 46 | [ 47 | ['V', 'H', Cx(1)], 48 | ['H', 'V', Cx(1)], 49 | ], 50 | [Dimension.polarization()], 51 | ) 52 | expect(opX.toString()).toBe( 53 | 'Operator with 2 entries of max size [[2], [2]] with dimensions [[polarization], [polarization]]\n' + 54 | '(1.00 +0.00i) |V⟩⟨H| + (1.00 +0.00i) |H⟩⟨V|\n', 55 | ) 56 | 57 | const opFromSparse = Operator.fromSparseCoordNames( 58 | [ 59 | ['uH', 'uH', Cx(0, 2)], 60 | ['dH', 'uV', Cx(-1, -1)], 61 | ['dV', 'dH', Cx(0.5, 2.5)], 62 | ], 63 | [Dimension.spin(), Dimension.polarization()], 64 | ) 65 | expect(opFromSparse.toString('cartesian', 2, ' + ', false)).toEqual( 66 | '(0.00 +2.00i) |u,H⟩⟨u,H| + (-1.00 -1.00i) |d,H⟩⟨u,V| + (0.50 +2.50i) |d,V⟩⟨d,H|', 67 | ) 68 | }) 69 | 70 | it('exports to sparse index index value', () => { 71 | const idPolDir = Operator.identity([Dimension.polarization(), Dimension.spin()]) 72 | expect(idPolDir.toIndexIndexValues()).toEqual([ 73 | { i: 0, j: 0, v: Cx(1) }, 74 | { i: 1, j: 1, v: Cx(1) }, 75 | { i: 2, j: 2, v: Cx(1) }, 76 | { i: 3, j: 3, v: Cx(1) }, 77 | ]) 78 | }) 79 | 80 | it('row and column representations', () => { 81 | const idPolDir = Operator.identity([Dimension.polarization(), Dimension.spin()]) 82 | expect(idPolDir.toVectorPerInput()[2].coord).toEqual(idPolDir.toVectorPerInput()[2].vector.entries[0].coord) 83 | expect(idPolDir.toVectorPerOutput()[2].coord).toEqual(idPolDir.toVectorPerOutput()[2].vector.entries[0].coord) 84 | 85 | const op = Operator.fromSparseCoordNames( 86 | [ 87 | ['dH', 'uH', Cx(0, 2)], 88 | ['uH', 'uH', Cx(-1, -1)], 89 | ['dV', 'uH', Cx(0.5, 2.5)], 90 | ], 91 | [Dimension.spin(), Dimension.polarization()], 92 | ) 93 | const vecsIn = op.toVectorPerInput() 94 | const vecsOut = op.toVectorPerOutput() 95 | expect(vecsIn.length).toEqual(1) 96 | expect(vecsOut.length).toEqual(3) 97 | 98 | const vecIn = Vector.fromSparseCoordNames( 99 | [ 100 | ['dH', Cx(0, 2)], 101 | ['uH', Cx(-1, -1)], 102 | ['dV', Cx(0.5, 2.5)], 103 | ], 104 | [Dimension.spin(), Dimension.polarization()], 105 | ) 106 | expect(vecsIn[0].vector).toEqual(vecIn) 107 | 108 | expect(op.transpose().toVectorPerInput()).toEqual(op.toVectorPerOutput()) 109 | }) 110 | 111 | it('complex and Hermitian conjugation', () => { 112 | const op = Operator.fromSparseCoordNames( 113 | [ 114 | ['dH', 'dH', Cx(0, 2)], 115 | ['dH', 'uH', Cx(-1, -1)], 116 | ['dV', 'uH', Cx(0.5, 2.5)], 117 | ], 118 | [Dimension.spin(), Dimension.polarization()], 119 | ) 120 | 121 | const opConj = Operator.fromSparseCoordNames( 122 | [ 123 | ['dH', 'dH', Cx(0, -2)], 124 | ['dH', 'uH', Cx(-1, 1)], 125 | ['dV', 'uH', Cx(0.5, -2.5)], 126 | ], 127 | [Dimension.spin(), Dimension.polarization()], 128 | ) 129 | 130 | const opDag = Operator.fromSparseCoordNames( 131 | [ 132 | ['dH', 'dH', Cx(0, -2)], 133 | ['uH', 'dH', Cx(-1, 1)], 134 | ['uH', 'dV', Cx(0.5, -2.5)], 135 | ], 136 | [Dimension.spin(), Dimension.polarization()], 137 | ) 138 | 139 | expect(op.conj()).operatorCloseToNumbers(opConj.toDense()) 140 | expect(op.dag()).operatorCloseToNumbers(opDag.toDense()) 141 | }) 142 | 143 | it('map values', () => { 144 | const op = Operator.fromSparseCoordNames( 145 | [ 146 | ['dH', 'dH', Cx(0, 2)], 147 | ['dH', 'uH', Cx(-1, -1)], 148 | ['dV', 'uH', Cx(0.5, 2.5)], 149 | ], 150 | [Dimension.spin(), Dimension.polarization()], 151 | ) 152 | 153 | const opAbs2 = Operator.fromSparseCoordNames( 154 | [ 155 | ['dH', 'dH', Cx(4)], 156 | ['dH', 'uH', Cx(2)], 157 | ['dV', 'uH', Cx(6.5)], 158 | ], 159 | [Dimension.spin(), Dimension.polarization()], 160 | ) 161 | 162 | expect(op.mapValues((z) => z.mul(z.conj()))).operatorCloseToNumbers(opAbs2.toDense()) 163 | }) 164 | 165 | it('permute an operator', () => { 166 | const op = Operator.fromSparseCoordNames( 167 | [ 168 | ['0H0', 'dH1', Cx(0, 2)], 169 | ['1H0', 'uH1', Cx(-1, -1)], 170 | ['0V1', 'uH2', Cx(0.5, 2.5)], 171 | ], 172 | [Dimension.qubit(), Dimension.polarization(), Dimension.position(3)], 173 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 174 | ) 175 | 176 | const opPerm = Operator.fromSparseCoordNames( 177 | [ 178 | ['H00', 'H1d', Cx(0, 2)], 179 | ['H01', 'H1u', Cx(-1, -1)], 180 | ['V10', 'H2u', Cx(0.5, 2.5)], 181 | ], 182 | [Dimension.polarization(), Dimension.position(3), Dimension.qubit()], 183 | [Dimension.polarization(), Dimension.position(3), Dimension.spin()], 184 | ) 185 | 186 | const permuted = op.permute([1, 2, 0]) 187 | expect(permuted.namesOut).toEqual(['polarization', 'x', 'qubit']) 188 | expect(permuted.namesIn).toEqual(['polarization', 'x', 'spin']) 189 | expect(permuted).operatorCloseToNumbers(opPerm.toDense()) 190 | expect(() => op.permute([0, 1, 2, 3])).toThrowError('0,1,2,3 is not a valid permutation for 3 output dimensions.') 191 | expect(() => op.permute([0, 0, 0])).toThrowError('0,0,0 is not a valid permutation for 3 output dimensions.') 192 | expect(() => op.permute([2, 0, 0])).toThrowError('2,0,0 is not a valid permutation for 3 output dimensions.') 193 | }) 194 | 195 | it('permute an operator: in and out', () => { 196 | const op = Operator.fromSparseCoordNames( 197 | [ 198 | ['0H', 'dH1', Cx(0, 2)], 199 | ['1H', 'uH1', Cx(-1, -1)], 200 | ['0V', 'uH2', Cx(0.5, 2.5)], 201 | ], 202 | [Dimension.qubit(), Dimension.polarization()], 203 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 204 | ) 205 | 206 | const opPermOut = Operator.fromSparseCoordNames( 207 | [ 208 | ['H0', 'dH1', Cx(0, 2)], 209 | ['H1', 'uH1', Cx(-1, -1)], 210 | ['V0', 'uH2', Cx(0.5, 2.5)], 211 | ], 212 | [Dimension.polarization(), Dimension.qubit()], 213 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 214 | ) 215 | 216 | const opPermIn = Operator.fromSparseCoordNames( 217 | [ 218 | ['0H', 'H1d', Cx(0, 2)], 219 | ['1H', 'H1u', Cx(-1, -1)], 220 | ['0V', 'H2u', Cx(0.5, 2.5)], 221 | ], 222 | [Dimension.qubit(), Dimension.polarization()], 223 | [Dimension.polarization(), Dimension.position(3), Dimension.spin()], 224 | ) 225 | const permutedOut = op.permuteDimsOut([1, 0]) 226 | expect(permutedOut.namesOut).toEqual(['polarization', 'qubit']) 227 | expect(permutedOut.namesIn).toEqual(['spin', 'polarization', 'x']) // unchanged 228 | expect(permutedOut).operatorCloseToNumbers(opPermOut.toDense()) 229 | 230 | const permutedIn = op.permuteDimsIn([1, 2, 0]) 231 | expect(permutedIn.namesOut).toEqual(['qubit', 'polarization']) // unchanged 232 | expect(permutedIn.namesIn).toEqual(['polarization', 'x', 'spin']) 233 | expect(permutedIn).operatorCloseToNumbers(opPermIn.toDense()) 234 | 235 | expect(() => op.permuteDimsOut([1, 2, 0])).toThrowError('1,2,0 is not a valid permutation for 2 output dimensions.') 236 | expect(() => op.permuteDimsIn([1, 0])).toThrowError('1,0 is not a valid permutation for 3 input dimensions.') 237 | }) 238 | 239 | it('op-vec multiplication', () => { 240 | const dims = [Dimension.spin(), Dimension.polarization()] 241 | const id = Operator.identity(dims) 242 | const op = Operator.fromSparseCoordNames( 243 | [ 244 | ['dH', 'dH', Cx(0, 2)], 245 | ['dH', 'uH', Cx(-1, -1)], 246 | ['dV', 'uH', Cx(0.5, 2.5)], 247 | ], 248 | dims, 249 | ) 250 | const vec = Vector.fromSparseCoordNames( 251 | [ 252 | ['dH', Cx(0, 1)], 253 | ['uH', Cx(2, 0)], 254 | ['dV', Cx(0, 1)], 255 | ], 256 | dims, 257 | ) 258 | expect(id.mulVec(vec).toDense()).toEqual(vec.toDense()) 259 | const vec2 = Vector.fromSparseCoordNames( 260 | [ 261 | ['dH', Cx(-4, -2)], 262 | ['dV', Cx(1, 5)], 263 | ], 264 | dims, 265 | ) 266 | expect(op.mulVec(vec).toKetString('cartesian')).toEqual('(-4.00 -2.00i) |d,H⟩ + (1.00 +5.00i) |d,V⟩') 267 | expect(op.mulVec(vec).toDense()).toEqual(vec2.toDense()) 268 | }) 269 | 270 | it('op-op multiplication', () => { 271 | const dims = [Dimension.spin(), Dimension.polarization()] 272 | const id = Operator.identity(dims) 273 | const op = Operator.fromSparseCoordNames( 274 | [ 275 | ['dH', 'dH', Cx(0, 2)], 276 | ['dH', 'uH', Cx(-1, -1)], 277 | ['dV', 'uH', Cx(0.5, 2.5)], 278 | ], 279 | dims, 280 | ) 281 | expect(id.mulOp(op).toDense()).toEqual(op.toDense()) 282 | expect(op.mulOp(id).toDense()).toEqual(op.toDense()) 283 | const op2 = Operator.fromSparseCoordNames( 284 | [ 285 | ['uH', 'dH', Cx(1)], 286 | ['dH', 'uH', Cx(1)], 287 | ['dV', 'dV', Cx(0, 1)], 288 | ], 289 | dims, 290 | ) 291 | const op2right = Operator.fromSparseCoordNames( 292 | [ 293 | ['dH', 'uH', Cx(0, 2)], 294 | ['dH', 'dH', Cx(-1, -1)], 295 | ['dV', 'dH', Cx(0.5, 2.5)], 296 | ], 297 | dims, 298 | ) 299 | const op2left = Operator.fromSparseCoordNames( 300 | [ 301 | ['uH', 'dH', Cx(0, 2)], 302 | ['uH', 'uH', Cx(-1, -1)], 303 | ['dV', 'uH', Cx(-2.5, 0.5)], 304 | ], 305 | dims, 306 | ) 307 | expect(op.mulOp(op2).toDense()).toEqual(op2right.toDense()) 308 | expect(op2.mulOp(op).toDense()).toEqual(op2left.toDense()) 309 | }) 310 | 311 | it('op partial vec multiplication', () => { 312 | const vec = Vector.fromSparseCoordNames( 313 | [ 314 | ['0dH0', Cx(0, 1)], 315 | ['1uH0', Cx(2, 0)], 316 | ['2dV1', Cx(0, 1)], 317 | ['3dH1', Cx(0, 1)], 318 | ['4uH2', Cx(2, 0)], 319 | ['5dV2', Cx(0, 1)], 320 | ['6dV3', Cx(0, 1)], 321 | ], 322 | [Dimension.position(7), Dimension.spin(), Dimension.polarization(), Dimension.position(5)], 323 | ) 324 | 325 | const idSpin = Operator.identity([Dimension.spin()]) 326 | expect(idSpin.mulVecPartial([1], vec)).vectorCloseTo(vec) 327 | expect(() => idSpin.mulVecPartial([2], vec)).toThrowError('Dimensions array order mismatch...') 328 | expect(() => idSpin.mulVecPartial([2, 3], vec)).toThrowError('Dimensions array size mismatch...') 329 | expect(() => idSpin.mulVecPartial([4], vec)).toThrowError() 330 | 331 | const op1 = Operator.fromSparseCoordNames( 332 | [ 333 | ['dH', 'dH', Cx(0, 2)], 334 | ['dH', 'uH', Cx(-1, -1)], 335 | ['dV', 'uH', Cx(0.5, 2.5)], 336 | ], 337 | [Dimension.spin(), Dimension.polarization()], 338 | ) 339 | const vec1res = Vector.fromSparseCoordNames( 340 | [ 341 | ['0dH0', Cx(-2, 0)], 342 | ['1dH0', Cx(-2, -2)], 343 | ['1dV0', Cx(1, 5)], 344 | ['3dH1', Cx(-2, 0)], 345 | ['4dH2', Cx(-2, -2)], 346 | ['4dV2', Cx(1, 5)], 347 | ], 348 | [Dimension.position(7), Dimension.spin(), Dimension.polarization(), Dimension.position(5)], 349 | ) 350 | expect(op1.mulVecPartial([1, 2], vec)).vectorCloseTo(vec1res) 351 | const op2a = Operator.fromSparseCoordNames( 352 | [ 353 | ['4', '2', Cx(1)], 354 | ['4', '5', Cx(1)], 355 | ['4', '6', Cx(1)], 356 | ], 357 | [Dimension.position(7)], 358 | ) 359 | const op2b = Operator.fromSparseCoordNames( 360 | [ 361 | ['1', '0', Cx(0)], 362 | ['1', '1', Cx(1)], 363 | ['1', '2', Cx(2)], 364 | ['1', '3', Cx(3)], 365 | ], 366 | [Dimension.position(5)], 367 | ) 368 | const vec2res = Vector.fromSparseCoordNames( 369 | [['4dV1', Cx(0, 6)]], 370 | [Dimension.position(7), Dimension.spin(), Dimension.polarization(), Dimension.position(5)], 371 | ) 372 | 373 | const vec2 = op2b.mulVecPartial([3], op2a.mulVecPartial([0], vec)) 374 | expect(vec2).vectorCloseTo(vec2res) 375 | }) 376 | 377 | it('outer', () => { 378 | const opX = Operator.fromSparseCoordNames( 379 | [ 380 | ['V', 'H', Cx(1)], 381 | ['H', 'V', Cx(1)], 382 | ], 383 | [Dimension.polarization()], 384 | ) 385 | const opZ = Operator.fromSparseCoordNames( 386 | [ 387 | ['0', '0', Cx(1)], 388 | ['1', '1', Cx(-1)], 389 | ], 390 | [Dimension.qubit()], 391 | ) 392 | const opY = Operator.fromSparseCoordNames( 393 | [ 394 | ['d', 'u', Cx(0, 1)], 395 | ['u', 'd', Cx(0, -1)], 396 | ], 397 | [Dimension.spin()], 398 | ) 399 | const opRes = Operator.fromSparseCoordNames( 400 | [ 401 | ['V0', 'H0', Cx(1)], 402 | ['V1', 'H1', Cx(-1)], 403 | ['H0', 'V0', Cx(1)], 404 | ['H1', 'V1', Cx(-1)], 405 | ], 406 | [Dimension.polarization(), Dimension.qubit()], 407 | ) 408 | const opOut = opX.outer(opZ) 409 | expect(opOut.dimensionsIn).toEqual([Dimension.polarization(), Dimension.qubit()]) 410 | expect(opOut.dimensionsOut).toEqual([Dimension.polarization(), Dimension.qubit()]) 411 | expect(opOut).operatorCloseToNumbers(opRes.toDense()) 412 | expect(Operator.outer([opX, opZ, opY])).operatorCloseToNumbers(opOut.outer(opY).toDense()) 413 | expect(Operator.outer([opY, opX, opZ])).operatorCloseToNumbers(opY.outer(opOut).toDense()) 414 | expect(Operator.outer([opY, opX, opZ])).not.operatorCloseToNumbers(opOut.outer(opY).toDense()) 415 | }) 416 | 417 | it('polarization: change all dims for operator', () => { 418 | const faradayRotator = Elements.faradayRotator(90) 419 | const rotatorRotated = faradayRotator.toBasisAll('polarization', 'LR') 420 | expect(rotatorRotated.dimensionsOut[0].name).toEqual('direction') 421 | expect(rotatorRotated.dimensionsOut[1].name).toEqual('polarization') 422 | expect(rotatorRotated.dimensionsOut[1].coordString).toEqual('LR') 423 | expect(rotatorRotated.dimensionsIn[0].name).toEqual('direction') 424 | expect(rotatorRotated.dimensionsIn[1].name).toEqual('polarization') 425 | expect(rotatorRotated.dimensionsIn[1].coordString).toEqual('LR') 426 | expect(rotatorRotated.toString('polarTau', 2, ' + ', false)).toEqual( 427 | '1.00 exp(0.88τi) |^,L⟩⟨^,L| + 1.00 exp(0.13τi) |^,R⟩⟨^,R|' + 428 | ' + 1.00 exp(0.13τi) |v,L⟩⟨v,L| + 1.00 exp(0.88τi) |v,R⟩⟨v,R|', 429 | ) 430 | }) 431 | 432 | it('contract left and right', () => { 433 | const op = Operator.fromSparseCoordNames( 434 | [ 435 | ['dH', 'dH', Cx(0, 2)], 436 | ['dH', 'uV', Cx(-1, -1)], 437 | ['dV', 'uH', Cx(0.5, 2.5)], 438 | ], 439 | [Dimension.spin(), Dimension.polarization()], 440 | ) 441 | const vec = Vector.fromSparseCoordNames( 442 | [ 443 | ['H', Cx(2)], 444 | ['V', Cx(1)], 445 | ], 446 | [Dimension.polarization()], 447 | ) 448 | expect(op.contractLeft([1], vec).toString()).toBe( 449 | 'Operator with 3 entries of max size [[2], [2,2]] with dimensions [[spin], [spin,polarization]]\n' + 450 | '(0.00 +4.00i) |d⟩⟨d,H| + (-2.00 -2.00i) |d⟩⟨u,V| + (0.50 +2.50i) |d⟩⟨u,H|\n', 451 | ) 452 | expect(op.contractRight([1], vec).toString()).toBe( 453 | 'Operator with 3 entries of max size [[2,2], [2]] with dimensions [[spin,polarization], [spin]]\n' + 454 | '(-1.00 -1.00i) |d,H⟩⟨u| + (0.00 +4.00i) |d,H⟩⟨d| + (1.00 +5.00i) |d,V⟩⟨u|\n', 455 | ) 456 | expect(op.contractLeft([1], vec).contractRight([1], vec).toString()).toBe( 457 | 'Operator with 2 entries of max size [[2], [2]] with dimensions [[spin], [spin]]\n' + 458 | '(-1.00 +3.00i) |d⟩⟨u| + (0.00 +8.00i) |d⟩⟨d|\n', 459 | ) 460 | }) 461 | 462 | it('creating projections', () => { 463 | const vec = Vector.fromSparseCoordNames( 464 | [ 465 | ['u', Cx(1, 0)], 466 | ['d', Cx(0, 1)], 467 | ], 468 | [Dimension.spin()], 469 | ).normalize() 470 | const proj = Operator.projectionOn(vec) 471 | expect(proj.isCloseToProjection()).toBe(true) 472 | expect(proj.toString()).toEqual( 473 | [ 474 | 'Operator with 4 entries of max size [[2], [2]] with dimensions [[spin], [spin]]', 475 | '(0.50 +0.00i) |u⟩⟨u| + (0.00 -0.50i) |u⟩⟨d| + (0.00 +0.50i) |d⟩⟨u| + (0.50 +0.00i) |d⟩⟨d|\n', 476 | ].join('\n'), 477 | ) 478 | }) 479 | 480 | it('trace for identity operator', () => { 481 | const op = Operator.identity([Dimension.spin(), Dimension.direction(), Dimension.position(3)]) 482 | expect(op.trace().re).toBeCloseTo(24) 483 | expect(op.trace().im).toBeCloseTo(0) 484 | }) 485 | 486 | it('trace', () => { 487 | const op = Operator.fromSparseCoordNames( 488 | [ 489 | ['dH', 'dH', Cx(0, 2)], 490 | ['dH', 'uV', Cx(-1, -1)], 491 | ['dV', 'uH', Cx(0.5, 2.5)], 492 | ['uV', 'uV', Cx(-0.5, 3.5)], 493 | ], 494 | [Dimension.spin(), Dimension.polarization()], 495 | ) 496 | expect(op.trace().re).toBeCloseTo(-0.5) 497 | expect(op.trace().im).toBeCloseTo(5.5) 498 | }) 499 | 500 | it('partial trace', () => { 501 | const op = Operator.fromSparseCoordNames( 502 | [ 503 | ['dH', 'dH', Cx(0, 2)], 504 | ['dH', 'uV', Cx(-1, -1)], 505 | ['dV', 'uH', Cx(0.5, 2.5)], 506 | ['uV', 'uH', Cx(1.5, -1.5)], 507 | ['uV', 'uV', Cx(-0.5, 3.5)], 508 | ], 509 | [Dimension.spin(), Dimension.polarization()], 510 | ) 511 | const ptA = op.partialTrace([1]) 512 | expect(ptA.toString()).toEqual( 513 | [ 514 | 'Operator with 2 entries of max size [[2], [2]] with dimensions [[spin], [spin]]', 515 | '(0.00 +2.00i) |d⟩⟨d| + (-0.50 +3.50i) |u⟩⟨u|\n', 516 | ].join('\n'), 517 | ) 518 | const ptB = op.partialTrace([0]) 519 | expect(ptB.toString()).toEqual( 520 | [ 521 | 'Operator with 3 entries of max size [[2], [2]] with dimensions [[polarization], [polarization]]', 522 | '(0.00 +2.00i) |H⟩⟨H| + (1.50 -1.50i) |V⟩⟨H| + (-0.50 +3.50i) |V⟩⟨V|\n', 523 | ].join('\n'), 524 | ) 525 | }) 526 | }) 527 | -------------------------------------------------------------------------------- /tests/Ops.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import * as Ops from '../src/Ops' 4 | import './customMatchers' 5 | 6 | describe('Auxiliary operators', () => { 7 | const TAU = 2 * Math.PI 8 | 9 | it('polarization', () => { 10 | expect(Ops.polStates['D'].toKetString('cartesian')).toBe('(0.71 +0.00i) |H⟩ + (0.71 +0.00i) |V⟩') 11 | expect(Ops.polStates['Z']).toBe(undefined) 12 | }) 13 | 14 | it('rotation', () => { 15 | expect(Ops.rotationMatrix(0, Dimension.qubit()).isCloseToIdentity()).toBe(true) 16 | expect(Ops.rotationMatrix(-13 * TAU, Dimension.qubit()).isCloseToIdentity()).toBe(true) 17 | const rot1 = Ops.rotationMatrix(1.3 * TAU, Dimension.qubit()) 18 | const rot2 = Ops.rotationMatrix(2.4 * TAU, Dimension.qubit()) 19 | const rot3 = Ops.rotationMatrix(0.7 * TAU, Dimension.qubit()) 20 | expect(rot1.mulOp(rot2)).operatorCloseToNumbers(rot3.toDense()) 21 | }) 22 | 23 | it('projection', () => { 24 | expect(Ops.projectionMatrix(13, Dimension.qubit()).isCloseToProjection()).toBe(true) 25 | const proj1 = Ops.projectionMatrix(4.2, Dimension.qubit()) 26 | const proj2 = Ops.projectionMatrix(4.2 + TAU / 2, Dimension.qubit()) 27 | const proj3 = Ops.projectionMatrix(4.2 + TAU / 4, Dimension.qubit()) 28 | expect(proj1.mulOp(proj2).isCloseToZero()).toBe(false) 29 | expect(proj1.mulOp(proj3).isCloseToZero()).toBe(true) 30 | }) 31 | 32 | it('phase shift', () => { 33 | expect(Ops.phaseShiftForRealEigenvectors(4.2, 0.13, 1.37, Dimension.qubit()).isCloseToUnitary()).toBe(true) 34 | const shift1 = Ops.phaseShiftForRealEigenvectors(4.2, -0.15, 0.3, Dimension.qubit()) 35 | const shift2 = Ops.phaseShiftForRealEigenvectors(4.2, 0.19, 0.4, Dimension.qubit()) 36 | const shift3 = Ops.phaseShiftForRealEigenvectors(4.2, 0.04, 0.7, Dimension.qubit()) 37 | expect(shift1.mulOp(shift2)).operatorCloseToNumbers(shift3.toDense()) 38 | }) 39 | 40 | it('reflection', () => { 41 | expect(Ops.reflectPhaseFromDenser().isCloseToUnitary()).toBe(true) 42 | expect(Ops.reflectPhaseFromDenser().isCloseToHermitian()).toBe(true) 43 | expect(Ops.reflectPhaseFromDenser().mulConstant(Cx(-1))).operatorCloseToNumbers( 44 | Ops.reflectPhaseFromLighter().toDense(), 45 | ) 46 | }) 47 | 48 | it('amplitude and intensity', () => { 49 | expect(Ops.amplitudeIntensity(1, 1.37).isCloseToUnitary()).toBe(true) 50 | expect(Ops.amplitudeIntensity(0.99, 1.37).isCloseToUnitary()).toBe(false) 51 | expect(Ops.amplitudeIntensity(0, 1.37).isCloseToZero()).toBe(true) 52 | expect(Ops.amplitudeIntensity(0.42, 0).isCloseToHermitian()).toBe(true) 53 | const amp1 = Ops.amplitudeIntensity(0.8, 1.4) 54 | const amp2 = Ops.amplitudeIntensity(0.3, -3.1) 55 | const amp3 = Ops.amplitudeIntensity(0.24, 0.3) 56 | expect(amp1.mulOp(amp2)).operatorCloseToNumbers(amp3.toDense()) 57 | }) 58 | 59 | it('reflections from a plane', () => { 60 | expect(Ops.reflectFromPlaneDirection(0).isCloseToUnitary()).toBe(false) 61 | expect(Ops.reflectFromPlaneDirection(0).isCloseToUnitaryOnSubspace()).toBe(true) 62 | expect(Ops.reflectFromPlaneDirection(45).isCloseToUnitary()).toBe(true) 63 | expect(Ops.reflectFromPlaneDirection(90).isCloseToUnitary()).toBe(false) 64 | expect(Ops.reflectFromPlaneDirection(90).isCloseToUnitaryOnSubspace()).toBe(true) 65 | expect(Ops.reflectFromPlaneDirection(135).isCloseToUnitary()).toBe(true) 66 | 67 | expect(Ops.reflectFromPlaneDirection(0).isCloseToHermitian()).toBe(true) 68 | expect(Ops.reflectFromPlaneDirection(45).isCloseToHermitian()).toBe(true) 69 | expect(Ops.reflectFromPlaneDirection(90).isCloseToHermitian()).toBe(true) 70 | expect(Ops.reflectFromPlaneDirection(135).isCloseToHermitian()).toBe(true) 71 | 72 | expect(() => Ops.reflectFromPlaneDirection(136)).toThrowError("Angle 136 % 180 isn't in the set [0, 45, 90, 135]") 73 | }) 74 | 75 | it('beam splitter transmittion', () => { 76 | expect(Ops.beamsplitterTransmittionDirections(0).isCloseToProjection()).toBe(true) 77 | expect(Ops.beamsplitterTransmittionDirections(90).isCloseToProjection()).toBe(true) 78 | expect(Ops.beamsplitterTransmittionDirections(45).isCloseToIdentity()).toBe(true) 79 | expect(Ops.beamsplitterTransmittionDirections(135).isCloseToIdentity()).toBe(true) 80 | expect(() => Ops.beamsplitterTransmittionDirections(136)).toThrowError( 81 | "Angle 136 % 180 isn't in the set [0, 45, 90, 135]", 82 | ) 83 | }) 84 | 85 | it('diode transmittion', () => { 86 | expect(Ops.diodeForDirections(0).isCloseToProjection()).toBe(true) 87 | expect(Ops.diodeForDirections(90).isCloseToProjection()).toBe(true) 88 | expect(Ops.diodeForDirections(180).isCloseToProjection()).toBe(true) 89 | expect(Ops.diodeForDirections(270).isCloseToProjection()).toBe(true) 90 | expect(() => Ops.diodeForDirections(45)).toThrowError("Angle 45 % 360 isn't in the set [0, 90, 180, 270].") 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/Simulation.test.ts: -------------------------------------------------------------------------------- 1 | import Simulation, { generateLaserIndicator } from '../src/Simulation' 2 | import Operator from '../src/Operator' 3 | import { Elem, IGrid } from '../src/interfaces' 4 | 5 | const grid1: IGrid = { 6 | cols: 13, 7 | rows: 10, 8 | cells: [ 9 | { 10 | x: 3, 11 | y: 2, 12 | element: Elem.Laser, 13 | rotation: 0, 14 | polarization: 0, 15 | }, 16 | { 17 | x: 9, 18 | y: 2, 19 | element: Elem.Detector, 20 | rotation: 180, 21 | polarization: 0, 22 | }, 23 | ], 24 | } 25 | 26 | describe('Simulation', () => { 27 | it('creates a simulation', () => { 28 | const sim = Simulation.fromGrid(grid1) 29 | expect(sim.frames).toStrictEqual([]) 30 | expect(sim.operators).toHaveLength(2) 31 | expect(sim.globalOperator).toBeInstanceOf(Operator) 32 | }) 33 | 34 | it('Creates an initial frame by firing the laz0rs', () => { 35 | const sim = Simulation.fromGrid(grid1) 36 | const laserIndicator = generateLaserIndicator(grid1.cells) 37 | sim.initializeFromIndicator(laserIndicator) 38 | expect(sim.frames).toHaveLength(1) 39 | expect(sim.frames[0].particles).toHaveLength(1) 40 | const photon = sim.frames[0].particles[0] 41 | expect(photon).toStrictEqual({ 42 | x: 3, 43 | y: 2, 44 | direction: 0, 45 | are: 1, 46 | aim: 0, 47 | bim: 0, 48 | bre: 0, 49 | }) 50 | }) 51 | 52 | it('should propagate the photon', () => { 53 | const sim = Simulation.fromGrid(grid1) 54 | const laserIndicator = generateLaserIndicator(grid1.cells) 55 | sim.initializeFromIndicator(laserIndicator) 56 | sim.frames.push(sim.nextFrame()) 57 | expect(sim.lastFrame.particles).toHaveLength(1) 58 | const photon = sim.lastFrame.particles[0] 59 | expect(photon).toStrictEqual({ 60 | x: 4, 61 | y: 2, 62 | direction: 0, 63 | are: 1, 64 | aim: 0, 65 | bim: 0, 66 | bre: 0, 67 | }) 68 | }) 69 | 70 | it('should generate frames', () => { 71 | const sim = Simulation.fromGrid(grid1) 72 | const laserIndicator = generateLaserIndicator(grid1.cells) 73 | sim.initializeFromIndicator(laserIndicator) 74 | sim.generateFrames() 75 | expect(sim.frames).toHaveLength(7) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/Vector.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import Dimension from '../src/Dimension' 3 | import VectorEntry from '../src/VectorEntry' 4 | import Vector from '../src/Vector' 5 | import './customMatchers' 6 | 7 | describe('Sparse Complex Vector', () => { 8 | const vector0 = Vector.fromArray([Cx(0), Cx(0), Cx(0), Cx(0)], [Dimension.spin(), Dimension.position(2)]) 9 | 10 | const vector = Vector.fromArray( 11 | [Cx(1, -1), Cx(2, -2), Cx(3, -3), Cx(0, 0)], 12 | [Dimension.spin(), Dimension.position(2)], 13 | ) 14 | 15 | const vector2 = Vector.fromArray( 16 | [Cx(0, 0), Cx(-2, 1), Cx(0, 0.5), Cx(0, 0)], 17 | [Dimension.spin(), Dimension.position(2)], 18 | ) 19 | 20 | it('throw error for fromArray of wrong size', () => { 21 | const array = [Cx(1, -1), Cx(2, -2), Cx(3, -3)] 22 | const dimensions = [Dimension.spin(), Dimension.position(2)] 23 | expect(() => Vector.fromArray(array, dimensions)).toThrowError('Dimension inconsistency: entry count 3 != total: 4') 24 | }) 25 | 26 | it('should create a vector from entries and dimensions', () => { 27 | const dimensions = [Dimension.spin(), Dimension.direction()] 28 | const entry1 = new VectorEntry([0, 3], Cx(1, -1)) 29 | const entry2 = new VectorEntry([1, 0], Cx(2, -2)) 30 | const entry3 = new VectorEntry([1, 3], Cx(3, -3)) 31 | const vector = new Vector([entry1, entry2, entry3], dimensions) 32 | expect(vector.entries.length).toEqual(3) 33 | }) 34 | 35 | it('should not create a vector if dims are incompatible', () => { 36 | const dimensions = [Dimension.spin(), Dimension.direction(), Dimension.polarization()] 37 | const entry1 = new VectorEntry([0, 3, 1], Cx(1, -1)) 38 | const entry2 = new VectorEntry([1, 1, 2], Cx(2, -2)) 39 | const entry3 = new VectorEntry([1, 3, 0], Cx(3, -3)) 40 | expect(() => new Vector([entry1, entry2, entry3], dimensions)).toThrowError( 41 | 'Coordinates [1,1,2] incompatible with sizes [2,4,2].', 42 | ) 43 | }) 44 | 45 | it('creates vector from sparse entries', () => { 46 | const vector = Vector.fromSparseCoordNames( 47 | [ 48 | ['H', Cx(1.23)], 49 | ['V', Cx(-1, -1)], 50 | ], 51 | [Dimension.polarization()], 52 | ) 53 | expect(vector.toKetString('cartesian')).toBe('(1.23 +0.00i) |H⟩ + (-1.00 -1.00i) |V⟩') 54 | 55 | const vector2 = Vector.fromSparseCoordNames( 56 | [ 57 | ['uH', Cx(1)], 58 | ['dV', Cx(-1)], 59 | ], 60 | [Dimension.spin(), Dimension.polarization()], 61 | ) 62 | expect(vector2).vectorCloseToNumbers([Cx(1), Cx(0), Cx(0), Cx(-1)]) 63 | }) 64 | 65 | it('should give vector getter properties', () => { 66 | expect(vector.size).toEqual([2, 2]) 67 | expect(vector.totalSize).toEqual(4) 68 | expect(vector.names).toEqual(['spin', 'x']) 69 | expect(vector.coordNames).toEqual([ 70 | ['u', 'd'], 71 | ['0', '1'], 72 | ]) 73 | }) 74 | 75 | it('should convert a vector to its dense representation', () => { 76 | expect(vector.toDense()).toEqual([Cx(1, -1), Cx(2, -2), Cx(3, -3), Cx(0, 0)]) 77 | }) 78 | 79 | it('should conjugate a vector', () => { 80 | expect(vector.conj().toDense()).toEqual([Cx(1, 1), Cx(2, 2), Cx(3, 3), Cx(0, 0)]) 81 | }) 82 | 83 | it('should permute a vector', () => { 84 | const permuted = vector.permute([1, 0]) 85 | expect(permuted.names).toEqual(['x', 'spin']) 86 | expect(permuted.toDense()).toEqual([Cx(1, -1), Cx(3, -3), Cx(2, -2), Cx(0, 0)]) 87 | expect(() => vector.permute([0, 1, 2])).toThrowError('0,1,2 is not a valid permutation for 2 dimensions.') 88 | expect(() => vector.permute([0, 0])).toThrowError('0,0 is not a valid permutation for 2 dimensions.') 89 | expect(() => vector.permute([2, 0])).toThrowError('2,0 is not a valid permutation for 2 dimensions.') 90 | }) 91 | 92 | it('should compute the norm squared of a vector', () => { 93 | expect(vector.normSquared()).toEqual(28) 94 | }) 95 | 96 | it('should normalize a vector', () => { 97 | const vector10 = Vector.fromArray( 98 | [Cx(1, 0), Cx(-3, -9), Cx(0, -3), Cx(0, 0)], 99 | [Dimension.spin(), Dimension.position(2)], 100 | ) 101 | expect(vector10.normalize()).vectorCloseToNumbers([Cx(0.1, 0), Cx(-0.3, -0.9), Cx(0, -0.3), Cx(0, 0)]) 102 | expect(() => vector0.normalize()).toThrowError('Cannot normalize a zero-length vector!') 103 | }) 104 | 105 | it('should add two vectors', () => { 106 | expect(vector.add(vector2).toDense()).toEqual([Cx(1, -1), Cx(0, -1), Cx(3, -2.5), Cx(0, 0)]) 107 | }) 108 | 109 | it('should scale a vector with a complex scalar', () => { 110 | const scalar1 = Cx(1, 0) 111 | expect(vector.mulConstant(scalar1).toDense()).toEqual(vector.toDense()) 112 | const scalar2 = Cx(-1, 0) 113 | expect(vector.mulConstant(scalar2).toDense()).toEqual([Cx(-1, 1), Cx(-2, 2), Cx(-3, 3), Cx(0, 0)]) 114 | const scalar3 = Cx(0, 1) 115 | expect(vector.mulConstant(scalar3).toDense()).toEqual([Cx(1, 1), Cx(2, 2), Cx(3, 3), Cx(0, 0)]) 116 | }) 117 | 118 | it('map values', () => { 119 | const vec = Vector.fromSparseCoordNames( 120 | [ 121 | ['dH', Cx(0, 2)], 122 | ['uH', Cx(-1, -1)], 123 | ['dV', Cx(0.5, 2.5)], 124 | ], 125 | [Dimension.spin(), Dimension.polarization()], 126 | ) 127 | 128 | const vecAbs2 = Vector.fromSparseCoordNames( 129 | [ 130 | ['dH', Cx(4)], 131 | ['uH', Cx(2)], 132 | ['dV', Cx(6.5)], 133 | ], 134 | [Dimension.spin(), Dimension.polarization()], 135 | ) 136 | 137 | expect(vec.mapValues((z) => z.mul(z.conj()))).vectorCloseTo(vecAbs2) 138 | }) 139 | 140 | it('should substract a vector from another one', () => { 141 | expect(vector.sub(vector).toDense()).toEqual([Cx(0, 0), Cx(0, 0), Cx(0, 0), Cx(0, 0)]) 142 | expect(vector.sub(vector2).toDense()).toEqual([Cx(1, -1), Cx(4, -3), Cx(3, -3.5), Cx(0, 0)]) 143 | }) 144 | 145 | it('should compute the dot product of two vectors', () => { 146 | expect(vector.dot(vector)).toEqual(Cx(0, -28)) 147 | expect(vector.dot(vector2)).toEqual(Cx(-0.5, 7.5)) 148 | }) 149 | 150 | it('should compute the inner product of two vectors', () => { 151 | expect(vector.inner(vector)).toEqual(Cx(28, 0)) 152 | expect(vector.inner(vector2)).toEqual(Cx(-7.5, -0.5)) 153 | }) 154 | 155 | it('partial dot and inner', () => { 156 | const vector = Vector.fromSparseCoordNames( 157 | [ 158 | ['dH0', Cx(1)], 159 | ['dV1', Cx(-1)], 160 | ['dV2', Cx(0, 1)], 161 | ], 162 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 163 | ) 164 | 165 | const vcs1 = vector.toGroupedByCoords([2]) 166 | expect(vcs1.length).toEqual(2) 167 | expect(vcs1[0].coord.length).toEqual(2) 168 | expect(vcs1[0].vector.size).toEqual([3]) 169 | 170 | const vcs2 = vector.toGroupedByCoords([0]) 171 | expect(vcs2.length).toEqual(3) 172 | expect(vcs2[0].coord.length).toEqual(2) 173 | expect(vcs2[0].vector.size).toEqual([2]) 174 | 175 | const vcs3 = vector.toGroupedByCoords([1, 2]) 176 | expect(vcs3.length).toEqual(1) 177 | expect(vcs3[0].coord.length).toEqual(1) 178 | expect(vcs3[0].vector.size).toEqual([2, 3]) 179 | expect(vcs3[0].vector.toKetString('cartesian')).toEqual( 180 | '(1.00 +0.00i) |H,0⟩ + (-1.00 +0.00i) |V,1⟩ + (0.00 +1.00i) |V,2⟩', 181 | ) 182 | }) 183 | 184 | it('partial dot and inner', () => { 185 | const vector = Vector.fromSparseCoordNames( 186 | [ 187 | ['dH0', Cx(1)], 188 | ['dV1', Cx(-1)], 189 | ['dV2', Cx(0, 1)], 190 | ], 191 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 192 | ) 193 | const smallVector1 = Vector.fromSparseCoordNames( 194 | [ 195 | ['0', Cx(10)], 196 | ['2', Cx(0, 3)], 197 | ], 198 | [Dimension.position(3)], 199 | ) 200 | const res1 = Vector.fromSparseCoordNames( 201 | [ 202 | ['dH', Cx(10)], 203 | ['dV', Cx(-3)], 204 | ], 205 | [Dimension.spin(), Dimension.polarization()], 206 | ) 207 | expect(smallVector1.dotPartial([2], vector).toKetString('cartesian')).toEqual( 208 | '(10.00 +0.00i) |d,H⟩ + (-3.00 +0.00i) |d,V⟩', 209 | ) 210 | expect(smallVector1.dotPartial([2], vector)).vectorCloseTo(res1) 211 | 212 | expect(smallVector1.innerPartial([2], vector).toKetString('cartesian')).toEqual( 213 | '(10.00 +0.00i) |d,H⟩ + (3.00 +0.00i) |d,V⟩', 214 | ) 215 | 216 | const smallVector2 = Vector.fromSparseCoordNames([['H', Cx(0, 1)]], [Dimension.polarization()]) 217 | const res2inner = Vector.fromSparseCoordNames([['d0', Cx(0, -1)]], [Dimension.spin(), Dimension.position(3)]) 218 | expect(smallVector2.innerPartial([1], vector).toString()).toEqual( 219 | 'Vector with 1 entries of max size [2,3] with dimensions [spin,x]\n(0.00 -1.00i) |d,0⟩\n', 220 | ) 221 | expect(smallVector2.innerPartial([1], vector)).vectorCloseToNumbers(res2inner.toDense()) 222 | 223 | const smallVector3 = Vector.fromSparseCoordNames([['u', Cx(0, 1)]], [Dimension.spin()]) 224 | expect(smallVector3.innerPartial([0], vector).normSquared()).toBeCloseTo(0) 225 | 226 | const smallVector4 = Vector.fromSparseCoordNames( 227 | [ 228 | ['d1', Cx(0, 1)], 229 | ['d2', Cx(0, 1)], 230 | ], 231 | [Dimension.spin(), Dimension.position(3)], 232 | ) 233 | expect(smallVector4.innerPartial([0, 2], vector).toString()).toEqual( 234 | 'Vector with 1 entries of max size [2] with dimensions [polarization]\n(1.00 +1.00i) |V⟩\n', 235 | ) 236 | }) 237 | 238 | it('should compute the outer product of two vectors', () => { 239 | const v1 = Vector.fromArray([Cx(0), Cx(1), Cx(2), Cx(3)], [Dimension.spin(), Dimension.position(2)]) 240 | const v2 = Vector.fromArray([Cx(1), Cx(0, 1), Cx(-1), Cx(0, -1)], [Dimension.spin(), Dimension.position(2)]) 241 | expect(v1.outer(v2)).vectorCloseToNumbers([ 242 | Cx(0), 243 | Cx(0), 244 | Cx(0), 245 | Cx(0), 246 | Cx(1, 0), 247 | Cx(0, 1), 248 | Cx(-1, 0), 249 | Cx(0, -1), 250 | Cx(2, 0), 251 | Cx(0, 2), 252 | Cx(-2, 0), 253 | Cx(0, -2), 254 | Cx(3, 0), 255 | Cx(0, 3), 256 | Cx(-3, 0), 257 | Cx(0, -3), 258 | ]) 259 | }) 260 | 261 | it('change all dims for vector', () => { 262 | const vector = Vector.fromSparseCoordNames( 263 | [ 264 | ['0u0HH', Cx(0.5)], 265 | ['0u0HV', Cx(0.5)], 266 | ['2u1VV', Cx(0.5)], 267 | ['2d1VV', Cx(0.0, -0.5)], 268 | ], 269 | [Dimension.position(5), Dimension.spin(), Dimension.qubit(), Dimension.polarization(), Dimension.polarization()], 270 | ) 271 | 272 | expect(vector.toBasisAll('polarization', 'HV').toKetString('cartesian')).toEqual( 273 | '(0.50 +0.00i) |0,u,0,H,H⟩ + (0.50 +0.00i) |0,u,0,H,V⟩ + (0.50 +0.00i) |2,u,1,V,V⟩ + (0.00 -0.50i) |2,d,1,V,V⟩', 274 | ) 275 | expect(vector.toBasisAll('polarization', 'DA').toKetString('cartesian')).toEqual( 276 | '(0.50 +0.00i) |0,u,0,D,D⟩ + (-0.50 +0.00i) |0,u,0,A,D⟩ + (0.25 +0.00i) |2,u,1,D,D⟩ + (0.25 +0.00i) |2,u,1,D,A⟩' + 277 | ' + (0.25 +0.00i) |2,u,1,A,D⟩ + (0.25 +0.00i) |2,u,1,A,A⟩ + (0.00 -0.25i) |2,d,1,D,D⟩' + 278 | ' + (0.00 -0.25i) |2,d,1,D,A⟩ + (0.00 -0.25i) |2,d,1,A,D⟩ + (0.00 -0.25i) |2,d,1,A,A⟩', 279 | ) 280 | expect(vector.toBasisAll('spin', 'spin-y').toKetString('cartesian')).toEqual( 281 | '(0.35 +0.00i) |0,uy,0,H,H⟩ + (0.35 +0.00i) |0,uy,0,H,V⟩ + (0.00 -0.35i) |0,dy,0,H,H⟩' + 282 | ' + (0.00 -0.35i) |0,dy,0,H,V⟩ + (0.00 -0.71i) |2,dy,1,V,V⟩', 283 | ) 284 | }) 285 | 286 | it('creates a vector copy', () => { 287 | const vectorCopy = vector.copy() 288 | expect(vectorCopy.toDense()).toEqual(vector.toDense()) 289 | vectorCopy.entries[0].value.re = 999 290 | expect(vector.entries[0].value.re).toEqual(1) 291 | vectorCopy.dimensions[0].name = 'qqq' 292 | expect(vector.dimensions[0].name).toEqual('spin') 293 | }) 294 | 295 | it('creates string', () => { 296 | // eslint-disable-next-line prettier/prettier, max-len 297 | expect(vector.toString()).toEqual('Vector with 3 entries of max size [2,2] with dimensions [spin,x]\n(1.00 -1.00i) |u,0⟩ + (2.00 -2.00i) |u,1⟩ + (3.00 -3.00i) |d,0⟩\n') 298 | expect(vector.toString('polarTau', 3, ' ', false)).toEqual( 299 | '1.414 exp(0.875τi) |u,0⟩ 2.828 exp(0.875τi) |u,1⟩ 4.243 exp(0.875τi) |d,0⟩', 300 | ) 301 | }) 302 | 303 | it('creates simplified ket string', () => { 304 | expect(vector.toKetString()).toEqual('1.41 exp(0.88τi) |u,0⟩ + 2.83 exp(0.88τi) |u,1⟩ + 4.24 exp(0.88τi) |d,0⟩') 305 | }) 306 | 307 | it('creates index values output', () => { 308 | expect(vector.toIndexValues()).toEqual([ 309 | { i: 0, v: Cx(1, -1) }, 310 | { i: 1, v: Cx(2, -2) }, 311 | { i: 2, v: Cx(3, -3) }, 312 | ]) 313 | }) 314 | 315 | it('ket components output', () => { 316 | const vector = Vector.fromSparseCoordNames( 317 | [ 318 | ['dH0', Cx(1)], 319 | ['dV1', Cx(-1)], 320 | ['dV2', Cx(0, 1)], 321 | ], 322 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 323 | ).toBasisAll('spin', 'spin-x') 324 | const ketComponents = vector.toKetComponents() 325 | expect(ketComponents.length).toBe(6) 326 | expect(ketComponents[0].amplitude.re).toBeCloseTo(Math.SQRT1_2) 327 | expect(ketComponents[0].coordStrs).toEqual(['ux', 'H', '0']) 328 | }) 329 | 330 | it('ket components output - keeping order', () => { 331 | const vector = Vector.fromSparseCoordNames( 332 | [ 333 | ['dV1', Cx(-1)], 334 | ['dH0', Cx(1)], 335 | ['dV2', Cx(0, 1)], 336 | ], 337 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 338 | ).toBasisAll('spin', 'spin-x') 339 | const ketComponents = vector.toKetComponents() 340 | expect(ketComponents[0].amplitude.re).toBeCloseTo(Math.SQRT1_2) 341 | expect(ketComponents[0].coordStrs).toEqual(['ux', 'H', '0']) 342 | }) 343 | 344 | it('creates vector with a single entry', () => { 345 | const dims = [Dimension.polarization(), Dimension.spin()] 346 | const vec = Vector.indicator(dims, 'Hd') 347 | expect(vec.entries.length).toEqual(1) 348 | expect(vec.entries[0].value).toEqual(Cx(1)) 349 | expect(() => Vector.indicator(dims, 'H')).toThrowError('') 350 | expect(() => Vector.indicator(dims, 'dH')).toThrowError('') 351 | }) 352 | 353 | it('does outer product for many entires', () => { 354 | const outer = Vector.outer([ 355 | Vector.fromArray([Cx(1), Cx(-1)], [Dimension.spin()]), 356 | Vector.fromArray([Cx(0), Cx(0, 1), Cx(0), Cx(0)], [Dimension.direction()]), 357 | Vector.fromArray([Cx(1), Cx(0), Cx(2)], [Dimension.position(3)]), 358 | ]) 359 | expect(outer.size).toEqual([2, 4, 3]) 360 | // eslint-disable-next-line prettier/prettier, max-len 361 | expect(outer.toString()).toEqual('Vector with 4 entries of max size [2,4,3] with dimensions [spin,direction,x]\n(0.00 +1.00i) |u,^,0⟩ + (0.00 +2.00i) |u,^,2⟩ + (0.00 -1.00i) |d,^,0⟩ + (0.00 -2.00i) |d,^,2⟩\n') 362 | }) 363 | 364 | it('does sum product for many entires', () => { 365 | const sum = Vector.add([ 366 | Vector.fromArray([Cx(1), Cx(-1)], [Dimension.spin()]), 367 | Vector.fromArray([Cx(3, 2), Cx(1, 1)], [Dimension.spin()]), 368 | Vector.fromArray([Cx(1), Cx(0)], [Dimension.spin()]), 369 | ]) 370 | expect(sum.toDense()).toEqual([Cx(5, 2), Cx(0, 1)]) 371 | }) 372 | 373 | it('vector of ones', () => { 374 | const ones = Vector.ones([Dimension.spin(), Dimension.direction()]) 375 | expect(ones.toKetString()).toEqual( 376 | // eslint-disable-next-line max-len 377 | '1.00 exp(0.00τi) |u,>⟩ + 1.00 exp(0.00τi) |u,^⟩ + 1.00 exp(0.00τi) |u,<⟩ + 1.00 exp(0.00τi) |u,v⟩ + 1.00 exp(0.00τi) |d,>⟩ + 1.00 exp(0.00τi) |d,^⟩ + 1.00 exp(0.00τi) |d,<⟩ + 1.00 exp(0.00τi) |d,v⟩', 378 | ) 379 | }) 380 | 381 | it('random, dense vector', () => { 382 | const rand = Vector.random([Dimension.spin(), Dimension.polarization()]) 383 | // expect(rand.toKetString()).toEqual('') 384 | // e.g.: '0.57 exp(0.66τi) |u,H⟩ + 0.16 exp(0.17τi) |u,V⟩ + 0.24 exp(0.02τi) |d,H⟩ + 0.77 exp(0.49τi) |d,V⟩' 385 | expect(rand.entries.length).toEqual(4) 386 | }) 387 | 388 | it('random vector from a subspace', () => { 389 | const vector = Vector.fromSparseCoordNames( 390 | [ 391 | ['dV1', Cx(-1)], 392 | ['dH0', Cx(1)], 393 | ['dV2', Cx(0, 1)], 394 | ], 395 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 396 | ) 397 | const rand = vector.randomOnSubspace() 398 | // expect(rand.toKetString()).toEqual('') 399 | // e.g.: '0.62 exp(0.23τi) |d,H,0⟩ + 0.58 exp(0.69τi) |d,V,1⟩ + 0.53 exp(0.93τi) |d,V,2⟩' 400 | expect(rand.entries.length).toEqual(3) 401 | }) 402 | 403 | it('random vector from a partial subspace', () => { 404 | const vector = Vector.fromSparseCoordNames( 405 | [ 406 | ['dV1', Cx(-1)], 407 | ['dH0', Cx(1)], 408 | ['dV2', Cx(0, 1)], 409 | ], 410 | [Dimension.spin(), Dimension.polarization(), Dimension.position(3)], 411 | ) 412 | const rand0 = vector.randomOnPartialSubspace([0]) 413 | // expect(rand0.toKetString()).toEqual('') 414 | // e.g.: '1.00 exp(0.12τi) |d⟩' 415 | expect(rand0.entries.length).toEqual(1) 416 | 417 | const rand2 = vector.randomOnPartialSubspace([2]) 418 | // expect(rand2.toKetString()).toEqual('') 419 | // e.g.: '0.61 exp(0.69τi) |0⟩ + 0.10 exp(0.12τi) |1⟩ + 0.79 exp(0.10τi) |2⟩' 420 | expect(rand2.entries.length).toEqual(3) 421 | 422 | const rand12 = vector.randomOnPartialSubspace([1, 2]) 423 | //expect(rand12.toKetString()).toEqual('') 424 | // e.g.: '0.44 exp(0.65τi) |H,0⟩ + 0.68 exp(0.49τi) |V,1⟩ + 0.59 exp(0.79τi) |V,2⟩' 425 | expect(rand12.entries.length).toEqual(3) 426 | 427 | const rand01 = vector.randomOnPartialSubspace([0, 1]) 428 | // expect(rand01.toKetString()).toEqual('') 429 | // e.g.: '0.46 exp(0.67τi) |d,H⟩ + 0.89 exp(0.68τi) |d,V⟩' 430 | expect(rand01.entries.length).toEqual(2) 431 | }) 432 | }) 433 | -------------------------------------------------------------------------------- /tests/VectorEntry.test.ts: -------------------------------------------------------------------------------- 1 | import { Cx } from '../src/Complex' 2 | import VectorEntry from '../src/VectorEntry' 3 | 4 | // Sparse cell testing 5 | describe('VectorEntry', () => { 6 | it('should convert from index (uid) to coordinates of dimension array', () => { 7 | const complex = Cx(4, -4) 8 | const cell = VectorEntry.fromIndexValue(23, [2, 4, 2], complex) 9 | expect(cell.coord).toEqual([0, 3, 1]) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /tests/customMatchers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | /* eslint-disable @typescript-eslint/interface-name-prefix */ 3 | import Complex from '../src/Complex' 4 | import Vector from '../src/Vector' 5 | import Operator from '../src/Operator' 6 | 7 | export {} 8 | 9 | declare global { 10 | namespace jest { 11 | interface Matchers { 12 | myMatcher: (received: string) => CustomMatcherResult 13 | vectorCloseTo: (received: Vector, eps?: number) => CustomMatcherResult 14 | vectorCloseToNumbers: (received: Complex[], eps?: number) => CustomMatcherResult 15 | operatorCloseToNumbers: (received: Complex[][], eps?: number) => CustomMatcherResult 16 | } 17 | } 18 | } 19 | 20 | /** 21 | * An example, from https://stackoverflow.com/questions/43667085/extending-third-party-module-that-is-globally-exposed. 22 | * @param this 23 | * @param received 24 | * @param expected 25 | */ 26 | function myMatcher(this: jest.MatcherUtils, received: string, expected: string): jest.CustomMatcherResult { 27 | const pass = received === expected 28 | return { 29 | pass, 30 | message: (): string => `Expected ${received}\nReceived: ${expected}`, 31 | } 32 | } 33 | 34 | /** 35 | * 36 | * @param this 37 | * @param received 38 | * @param expected 39 | */ 40 | function vectorCloseTo( 41 | this: jest.MatcherUtils, 42 | received: Vector, 43 | expected: Vector, 44 | eps = 1e-6, 45 | ): jest.CustomMatcherResult { 46 | const pass = received.sub(expected).norm < eps 47 | return { 48 | pass, 49 | message: (): string => `Expected ${received.toString()}\nReceived: ${expected.toString()}`, 50 | } 51 | } 52 | 53 | /** 54 | * 55 | * @param this 56 | * @param received 57 | * @param expected 58 | */ 59 | function vectorCloseToNumbers( 60 | this: jest.MatcherUtils, 61 | received: Vector, 62 | expected: Complex[], 63 | eps = 1e-6, 64 | ): jest.CustomMatcherResult { 65 | const receivedDense = received.toDense() 66 | let pass: boolean 67 | if (receivedDense.length !== expected.length) { 68 | pass = false 69 | } else { 70 | pass = receivedDense.map((c, i) => c.isCloseTo(expected[i], eps)).reduce((a, b) => a && b, true) 71 | } 72 | return { 73 | pass, 74 | message: (): string => `Expected ${receivedDense.toString()}\nReceived: ${expected.toString()}`, 75 | } 76 | } 77 | 78 | /** 79 | * 80 | * @param this 81 | * @param received 82 | * @param expected 83 | */ 84 | function operatorCloseToNumbers( 85 | this: jest.MatcherUtils, 86 | received: Operator, 87 | expected: Complex[][], 88 | eps = 1e-6, 89 | ): jest.CustomMatcherResult { 90 | const receivedDense = received.toDense() 91 | let pass: boolean 92 | if (receivedDense.length !== expected.length || receivedDense[0].length !== expected[0].length) { 93 | pass = false 94 | } else { 95 | pass = receivedDense 96 | .flatMap((row, i) => row.map((c, j) => c.isCloseTo(expected[i][j], eps))) 97 | .reduce((a, b) => a && b, true) 98 | } 99 | return { 100 | pass, 101 | message: (): string => `Expected ${receivedDense.toString()}\nReceived: ${expected.toString()}`, 102 | } 103 | } 104 | 105 | expect.extend({ 106 | myMatcher, 107 | vectorCloseTo, 108 | vectorCloseToNumbers, 109 | operatorCloseToNumbers, 110 | }) 111 | -------------------------------------------------------------------------------- /tests/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { coordsFromIndex, coordsToIndex, isPermutation } from '../src/helpers' 2 | 3 | describe('coordsFromIndex', () => { 4 | it('should map zero to zeros', () => { 5 | expect(coordsFromIndex(0, [2])).toEqual([0]) 6 | expect(coordsFromIndex(0, [3, 3])).toEqual([0, 0]) 7 | expect(coordsFromIndex(0, [5, 4, 3])).toEqual([0, 0, 0]) 8 | }) 9 | it('should work with binary', () => { 10 | const sizes = [2, 2, 2] 11 | expect(coordsFromIndex(0, sizes)).toEqual([0, 0, 0]) 12 | expect(coordsFromIndex(1, sizes)).toEqual([0, 0, 1]) 13 | expect(coordsFromIndex(2, sizes)).toEqual([0, 1, 0]) 14 | expect(coordsFromIndex(3, sizes)).toEqual([0, 1, 1]) 15 | expect(coordsFromIndex(4, sizes)).toEqual([1, 0, 0]) 16 | expect(coordsFromIndex(5, sizes)).toEqual([1, 0, 1]) 17 | expect(coordsFromIndex(6, sizes)).toEqual([1, 1, 0]) 18 | expect(coordsFromIndex(7, sizes)).toEqual([1, 1, 1]) 19 | }) 20 | it('should with other cases', () => { 21 | expect(coordsFromIndex(7, [5, 2, 3])).toEqual([1, 0, 1]) 22 | expect(coordsFromIndex(7, [9, 5, 2, 3])).toEqual([0, 1, 0, 1]) 23 | expect(coordsFromIndex(23, [7, 4, 3, 5])).toEqual([0, 1, 1, 3]) 24 | }) 25 | }) 26 | 27 | describe('coordsToIndex', () => { 28 | it('should map zero to zeros', () => { 29 | expect(coordsToIndex([0], [2])).toEqual(0) 30 | expect(coordsToIndex([0, 0], [3, 3])).toEqual(0) 31 | expect(coordsToIndex([0, 0, 0], [5, 4, 3])).toEqual(0) 32 | }) 33 | it('should work with binary', () => { 34 | const sizes = [2, 2, 2] 35 | expect(coordsToIndex([0, 0, 0], sizes)).toEqual(0) 36 | expect(coordsToIndex([0, 0, 1], sizes)).toEqual(1) 37 | expect(coordsToIndex([0, 1, 0], sizes)).toEqual(2) 38 | expect(coordsToIndex([0, 1, 1], sizes)).toEqual(3) 39 | expect(coordsToIndex([1, 0, 0], sizes)).toEqual(4) 40 | expect(coordsToIndex([1, 0, 1], sizes)).toEqual(5) 41 | expect(coordsToIndex([1, 1, 0], sizes)).toEqual(6) 42 | expect(coordsToIndex([1, 1, 1], sizes)).toEqual(7) 43 | }) 44 | it('should with other cases', () => { 45 | expect(coordsToIndex([1, 0, 1], [5, 2, 3])).toEqual(7) 46 | expect(coordsToIndex([0, 1, 0, 1], [9, 5, 2, 3])).toEqual(7) 47 | expect(coordsToIndex([0, 1, 1, 3], [7, 4, 3, 5])).toEqual(23) 48 | }) 49 | it('should be inverse of coordsFromIndex', () => { 50 | const sizes = [3, 2, 5, 4, 3, 5, 1] 51 | expect(coordsToIndex(coordsFromIndex(234, sizes), sizes)).toEqual(234) 52 | }) 53 | }) 54 | 55 | describe('isPermutation', () => { 56 | it('should work on sorted', () => { 57 | expect(isPermutation([0])).toEqual(true) 58 | expect(isPermutation([0, 1, 2, 3])).toEqual(true) 59 | }) 60 | 61 | it('should work on unsorted', () => { 62 | expect(isPermutation([2, 0, 1])).toEqual(true) 63 | expect(isPermutation([0, 3, 2, 4, 1])).toEqual(true) 64 | }) 65 | 66 | it('should show more or less elemens', () => { 67 | expect(isPermutation([2, 1, 1])).toEqual(false) 68 | expect(isPermutation([2, 1, 1, 1])).toEqual(false) 69 | expect(isPermutation([5, 3, 2, 4, 0])).toEqual(false) 70 | }) 71 | 72 | it('should check array size', () => { 73 | expect(isPermutation([0, 1, 2, 3], 3)).toEqual(false) 74 | expect(isPermutation([0, 1, 2, 3], 4)).toEqual(true) 75 | expect(isPermutation([0, 1, 2, 3], 5)).toEqual(false) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": false, 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 7 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | // "module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 9 | "lib": [ /* Specify library files to be included in the compilation. */ 10 | "ES2019", 11 | "dom" 12 | ], 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | "declarationDir": "./dist", 18 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 19 | "sourceMap": true, /* Generates corresponding '.map' file. */ 20 | // "outFile": "./", /* Concatenate and emit output to single file. */ 21 | "outDir": "./dist", /* Redirect output structure to the directory. */ 22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "composite": true, /* Enable project compilation */ 24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 25 | // "removeComments": true, /* Do not emit comments to output. */ 26 | // "noEmit": true, /* Do not emit outputs. */ 27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true, /* Enable all strict type-checking options. */ 33 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 34 | // "strictNullChecks": true, /* Enable strict null checks. */ 35 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 36 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 37 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 38 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 39 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | }, 68 | "include": [ 69 | "src/**/*.ts" 70 | ], 71 | "exclude": [ 72 | "node_modules" 73 | ] 74 | } 75 | --------------------------------------------------------------------------------