src/backends/simulators/jssim.js
/*
* Copyright (c) 2018 Isaac Phoenix (tearsofphoenix@icloud.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Contains a (slow) JavaScript simulator.
Please compile the c++ simulator for large-scale simulations.
*/
import assert from 'assert'
import math from 'mathjs'
import {
matrixDot,
matrixRangeAssign,
matrixRangeIndicesAssign,
zeros
} from '../../libs/util'
import {
len, setEqual, complexVectorDot
} from '../../libs/polyfill'
import { stringToArray } from '../../ops/qubitoperator'
/**
* @class JSSimulator
* @desc
NodeJS implementation of a quantum computer simulator.
This Simulator can be used as a backup if compiling the c++ simulator is
not an option (for some reason). It has the same features but is much
slower, so please consider building the c++ version for larger experiments.
*/
export default class Simulator {
/**
* @constructor
*/
constructor() {
// ignore seed
this._state = math.ones(1)
this._map = {}
this._numQubits = 0
}
/**
Return the qubit index to bit location map and the corresponding state
vector.
This function can be used to measure expectation values more efficiently (emulation).
@return {Array}
A tuple where the first entry is a dictionary mapping qubit indices
to bit-locations and the second entry is the corresponding state
vector
*/
cheat() {
return [this._map, this._state._data.slice(0)]
}
/**
Measure the qubits with IDs ids and return a list of measurement
outcomes (true/false).
@param {number[]} ids List of qubit IDs to measure.
@return {boolean[]} List of measurement results (containing either true or false).
*/
measureQubits(ids) {
const P = Math.random()
let val = 0.0
let i_picked = 0
while (val < P && i_picked < len(this._state)) {
val = math.add(val, math.abs(this._getState(i_picked) || math.complex(0, 0)) ** 2)
i_picked += 1
}
i_picked -= 1
const res = []
const pos = ids.map((ID) => {
res.push(false)
return this._map[ID]
})
let mask = 0
val = 0
pos.forEach((looper, i) => {
res[i] = ((i_picked >> looper) & 1) === 1
mask |= (1 << looper)
val |= ((res[i] & 1) << looper)
})
let nrm = 0.0
this._state.forEach((looper, _i) => {
const i = _i[0]
if ((mask & i) !== val) {
this._setState(i, 0.0)
} else {
const tmp = math.abs(looper)
nrm = math.add(nrm, math.multiply(tmp, tmp))
}
})
// normalize
const scale = 1.0 / Math.sqrt(nrm)
this._state = math.multiply(this._state, scale)
return res
}
/**
Allocate a qubit.
@param {number} ID ID of the qubit which is being allocated.
*/
allocateQubit(ID) {
this._map[ID] = this._numQubits
this._numQubits += 1
this._state.resize([1 << this._numQubits], 0)
}
/**
Return the classical value of a classical bit (i.e., a qubit which has
been measured / uncomputed).
@param {number} ID ID of the qubit of which to get the classical value.
@param {number} tolerance Tolerance for numerical errors when determining
whether the qubit is indeed classical.
@throws {Error} If the qubit is in a superposition, i.e., has not been measured / uncomputed.
*/
getClassicalValue(ID, tolerance = 1.e-10) {
const pos = this._map[ID]
let up = false
let down = false
for (let i = 0; i < len(this._state); i += (1 << (pos + 1))) {
for (let j = 0; j < (1 << pos); ++j) {
if (math.abs(this._getState(i + j)) > tolerance) {
up = true
}
if (math.abs(this._getState(i + j + (1 << pos)) || 0) > tolerance) {
down = true
}
if (up && down) {
throw new Error('Qubit has not been measured / '
+ 'uncomputed. Cannot access its '
+ 'classical value and/or deallocate a '
+ 'qubit in superposition!')
}
}
}
return down
}
/**
Deallocate a qubit (if it has been measured / uncomputed).
@param {number} ID ID of the qubit to deallocate.
@throws {Error} If the qubit is in a superposition, i.e., has not been measured / uncomputed.
*/
deallocateQubit(ID) {
const pos = this._map[ID]
const cv = this.getClassicalValue(ID)
const newstate = math.zeros(1 << (this._numQubits - 1))
let k = 0
for (let i = (1 << pos) * cv; i < len(this._state); i += 1 << (pos + 1)) {
matrixRangeIndicesAssign(newstate, k, k + (1 << pos), this._state, i)
k += (1 << pos)
}
const newmap = {}
Object.keys(this._map).forEach((key) => {
const value = this._map[key]
if (value > pos) {
newmap[key] = value - 1
} else if (parseInt(key, 10) !== ID) {
newmap[key] = value
}
})
this._map = newmap
this._state = newstate
this._numQubits -= 1
}
/**
Get control mask from list of control qubit IDs.
@return {number} A mask which represents the control qubits in binary.
*/
getControlMask(ctrlids) {
let mask = 0
ctrlids.forEach((ctrlid) => {
const ctrlpos = this._map[ctrlid]
mask |= (1 << ctrlpos)
})
return mask
}
/**
Emulate a math function (e.g., BasicMathGate).
@param {function} f Function executing the operation to emulate.
@param {Array.<number[]>} qubitIDs List of lists of qubit IDs to which
the gate is being applied. Every gate is applied to a tuple of
quantum registers, which corresponds to this 'list of lists'.
@param {number[]} ctrlQubitIDs List of control qubit ids.
*/
emulateMath(f, qubitIDs, ctrlQubitIDs) {
const mask = this.getControlMask(ctrlQubitIDs)
// determine qubit locations from their IDs
const qb_locs = []
qubitIDs.forEach((qureg) => {
qb_locs.push([])
qureg.forEach((qubitID) => {
qb_locs[qb_locs.length - 1].push(this._map[qubitID])
})
})
const newstate = math.zeros(len(this._state))
this._state.forEach((looper, _i) => {
const i = _i[0]
if ((mask & i) === mask) {
const argList = zeros(qb_locs.length)
qb_locs.forEach((qb, qri) => {
qb.forEach((il, qi) => {
argList[qri] |= (((i >> il) & 1) << qi)
})
})
const res = f(argList)
let newI = i
qb_locs.forEach((qb, qri) => {
qb.forEach((il, qi) => {
if (!(((newI >> il) & 1) == ((res[qri] >> qi) & 1))) {
newI ^= (1 << il)
}
})
})
newstate.subset(math.index(newI), looper)
} else {
newstate.subset(math.index(i), looper)
}
})
this._state = newstate
}
/**
Return the expectation value of a qubit operator w.r.t. qubit ids.
@param {Array.<Array>} termsArray Operator Array (see QubitOperator.terms)
@param {number[]} IDs List of qubit ids upon which the operator acts.
@return Expectation value
*/
getExpectationValue(termsArray, IDs) {
let expectation = 0.0
const current_state = math.clone(this._state)
termsArray.forEach(([term, coefficient]) => {
this.applyTerm(term, IDs)
const tmp = complexVectorDot(current_state, this._state)
const delta = math.multiply(coefficient, tmp)
expectation = math.add(expectation, delta)
this._state = math.clone(current_state)
})
if (math.im(expectation) === 0) {
return math.re(expectation)
}
return expectation
}
/**
Apply a (possibly non-unitary) qubit operator to qubits.
@param {Array.<Array>} termsArray Operator array (see QubitOperator.terms)
@param {number[]} IDs List of qubit ids upon which the operator acts.
*/
applyQubitOperator(termsArray, IDs) {
let new_state = math.zeros(len(this._state))
const current_state = math.clone(this._state)
termsArray.forEach(([term, coefficient]) => {
this.applyTerm(term, IDs)
const temp = math.multiply(this._state, coefficient)
new_state = math.add(new_state, temp)
this._state = math.clone(current_state)
})
this._state = new_state
}
/**
Return the probability of the outcome `bit_string` when measuring
the qubits given by the list of ids.
@param {boolean[]|number[]} bitString Measurement outcome.
@param {number[]} IDs List of qubit ids determining the ordering.
@return Probability of measuring the provided bit string.
@throws {Error} if an unknown qubit id was provided.
*/
getProbability(bitString, IDs) {
const n = IDs.length
for (let i = 0; i < n; ++i) {
const id = IDs[i]
const v = this._map[id]
if (typeof v === 'undefined') {
throw new Error('get_probability(): Unknown qubit id. '
+ 'Please make sure you have called '
+ 'eng.flush().')
}
}
let mask = 0
let bit_str = 0
for (let i = 0; i < n; ++i) {
mask |= (1 << this._map[IDs[i]])
bit_str |= (bitString[i] << this._map[IDs[i]])
}
let probability = 0.0
this._state.forEach((val, _i) => {
const i = _i[0]
if ((i & mask) === bit_str) {
const e = val
probability += math.re(e) ** 2 + math.im(e) ** 2
}
})
return probability
}
/**
* @ignore
* @param i
* @return {*}
* @private
*/
_getState(i) {
return this._state.subset(math.index(i))
}
/**
* @ignore
* @param i
* @param value
* @private
*/
_setState(i, value) {
this._state.subset(math.index(i), value)
}
/**
Return the probability amplitude of the supplied `bit_string`.
The ordering is given by the list of qubit ids.
@param {boolean[]|number[]} bitString Computational basis state
@param {number[]} IDs List of qubit ids determining the ordering. Must contain all allocated qubits.
@return Probability amplitude of the provided bit string.
@throws {Error} if the second argument is not a permutation of all allocated qubits.
*/
getAmplitude(bitString, IDs) {
const s1 = new Set(IDs)
const s2 = new Set(Object.keys(this._map).map(k => parseInt(k, 10)))
if (!setEqual(s1, s2)) {
throw new Error('The second argument to get_amplitude() must'
+ ' be a permutation of all allocated qubits. '
+ 'Please make sure you have called '
+ 'eng.flush().')
}
let index = 0
IDs.forEach((item, i) => {
item = parseInt(item, 10)
index |= (bitString[i] << this._map[item])
})
const ret = this._getState(index)
return ret
}
/**
Applies exp(-i*time*H) to the wave function, i.e., evolves under
the Hamiltonian H for a given time. The terms in the Hamiltonian
are not required to commute.
This function computes the action of the matrix exponential using
ideas from Al-Mohy and Higham, 2011.
TODO: Implement better estimates for s.
@param {Array.<Array>} terms_dict Operator dictionary (see QubitOperator.terms) defining the Hamiltonian.
@param {number} time Time to evolve for
@param {number[]} ids A list of qubit IDs to which to apply the evolution.
@param {number[]} ctrlids A list of control qubit IDs.
*/
emulateTimeEvolution(terms_dict, time, ids, ctrlids) {
// Determine the (normalized) trace, which is nonzero only for identity
// terms:
let tr = 0
let sum = 0
const newTerms = []
terms_dict.forEach(([t, c]) => {
if (t.length === 0) {
tr += c
} else {
newTerms.push([t, c])
sum += Math.abs(c)
}
})
terms_dict = newTerms
const op_nrm = math.abs(time) * sum
// rescale the operator by s:
const s = Math.floor(op_nrm + 1)
const correction = math.exp(math.complex(0, -time * tr / (s * 1.0)))
const output_state = math.clone(this._state)
const mask = this.getControlMask(ctrlids)
for (let i = 0; i < s; ++i) {
let j = 0
let nrm_change = 1.0
let update
while (nrm_change > 1.e-12) {
const coeff = math.divide(math.complex(0, -time), s * (j + 1))
const current_state = math.clone(this._state)
update = 0
terms_dict.forEach(([t, c]) => {
this.applyTerm(t, ids)
this._state = math.multiply(this._state, c)
update = math.add(this._state, update)
// update += this._state
this._state = math.clone(current_state)
})
update = math.multiply(update, coeff)
this._state = update
update.forEach((value, [m]) => {
if ((m & mask) === mask) {
const idx = math.index(m)
const v = math.add(output_state.subset(idx), value)
output_state.subset(idx, v)
}
})
nrm_change = math.norm(update)
j += 1
}
update.forEach((value, [k]) => {
if ((k & mask) === mask) {
const idx = math.index(k)
const v = math.multiply(output_state.subset(idx), correction)
output_state.subset(idx, v)
}
})
this._state = math.clone(output_state)
}
}
/**
* leave it empty to keep same API with cpp simulator
*/
run() {
//
}
/**
Applies a QubitOperator term to the state vector. (Helper function for time evolution & expectation)
@param {Array} term One term of QubitOperator.terms
@param {number[]} ids Term index to Qubit ID mapping
@param {number[]} controlIDs Control qubit IDs
*/
applyTerm(term, ids, controlIDs = []) {
const X = [[0.0, 1.0], [1.0, 0.0]]
const Y = [[0.0, math.complex(0, -1)], [math.complex(0, 1), 0.0]]
const Z = [[1.0, 0.0], [0.0, -1.0]]
const gates = {X, Y, Z}
term.forEach((local_op) => {
const qb_id = ids[local_op[0]]
this.applyControlledGate(gates[local_op[1]], [qb_id], controlIDs)
})
}
/**
Applies the k-qubit gate matrix m to the qubits with indices ids,
using ctrlids as control qubits.
@param {Array.<Array.<number>>} m 2^k x 2^k complex matrix describing the k-qubit gate.
@param {number[]} ids A list containing the qubit IDs to which to apply the gate.
@param {number[]} ctrlids A list of control qubit IDs (i.e., the gate is only applied where these qubits are 1).
*/
applyControlledGate(m, ids, ctrlids) {
const mask = this.getControlMask(ctrlids)
if (len(m) === 2) {
const k = ids[0]
const pos = this._map[k] || this._map[k.toString()]
this._singleQubitGate(m, pos, mask)
} else {
const pos = ids.map(ID => this._map[ID])
this._multiQubitGate(m, pos, mask)
}
}
/**
Applies the single qubit gate matrix m to the qubit at position `pos`
using `mask` to identify control qubits.
@param {Array.<Array.<number>>} m 2x2 complex matrix describing the single-qubit gate.
@param {number} pos Bit-position of the qubit.
@param {number} mask Bit-mask where set bits indicate control qubits.
*/
_singleQubitGate(m, pos, mask) {
const kernel = (u, d, m) => {
const ma = math.add
const mm = math.multiply
d = d || math.complex(0, 0)
u = u || math.complex(0, 0)
const r1 = ma(mm(u, m[0][0]), mm(d, m[0][1]))
const r2 = ma(mm(u, m[1][0]), mm(d, m[1][1]))
return [r1, r2]
}
const step = 1 << (pos + 1)
for (let i = 0; i < len(this._state); i += step) {
for (let j = 0; j < (1 << pos); ++j) {
if (((i + j) & mask) === mask) {
const id1 = i + j
const id2 = id1 + (1 << pos)
const [r1, r2] = kernel(this._getState(id1), this._getState(id2), m)
this._setState(id1, r1)
this._setState(id2, r2)
}
}
}
}
/**
Applies the k-qubit gate matrix m to the qubits at `pos`
using `mask` to identify control qubits.
@param {Array.<number[]>} m 2^k x 2^k complex matrix describing the k-qubit gate.
@param {number[]} pos List of bit-positions of the qubits.
@param {number} mask Bit-mask where set bits indicate control qubits.
see follows the description in https://arxiv.org/abs/1704.01127
*/
_multiQubitGate(m, pos, mask) {
const inactive = Object.keys(this._map).map(k => parseInt(k, 10)).filter(p => !pos.includes(p))
const matrix = math.matrix(m)
const subvec = zeros(1 << pos.length)
const subvec_idx = zeros(subvec.length)
for (let c = 0; c < (1 << inactive.length); ++c) {
// determine base index (state of inactive qubits)
let base = 0
for (let i = 0; i < inactive.length; ++i) {
base |= ((c >> i) & 1) << inactive[i]
}
// check the control mask
if (mask !== (base & mask)) {
continue
}
// now gather all elements involved in mat-vec mul
for (let x = 0; x < subvec_idx.length; ++x) {
let offset = 0
for (let i = 0; i < pos.length; ++i) {
offset |= ((x >> i) & 1) << pos[i]
}
subvec_idx[x] = base | offset
subvec[x] = this._getState(subvec_idx[x]) || math.complex(0, 0)
}
// perform mat-vec mul
matrixRangeAssign(this._state, subvec_idx, matrixDot(matrix, subvec))
}
}
/**
Set wavefunction and qubit ordering.
@param {Complex[]} wavefunction Array of complex amplitudes describing the wavefunction (must be normalized).
@param {Array} ordering List of ids describing the new ordering of qubits
(i.e., the ordering of the provided wavefunction).
*/
setWavefunction(wavefunction, ordering) {
// wavefunction contains 2^n values for n qubits
assert(wavefunction.length === (1 << ordering.length))
// all qubits must have been allocated before
const f1 = ordering.filter((Id) => {
const v = this._map[Id]
return typeof v !== 'undefined'
}).length === ordering.length
const f2 = len(this._map) === ordering.length
if (!f1 || !f2) {
throw new Error('set_wavefunction(): Invalid mapping provided.'
+ ' Please make sure all qubits have been '
+ 'allocated previously (call eng.flush()).')
}
this._state = math.matrix(wavefunction)
const map = {}
for (let i = 0; i < ordering.length; ++i) {
map[ordering[i]] = i
}
this._map = map
}
/**
Collapse a quantum register onto a classical basis state.
@param {number[]} ids Qubit IDs to collapse.
@param {boolean[]} values Measurement outcome for each of the qubit IDs in `ids`.
@throws {Error} If probability of outcome is ~0 or unknown qubits are provided.
*/
collapseWavefunction(ids, values) {
assert(ids.length === values.length)
// all qubits must have been allocated before
const f1 = ids.filter(Id => typeof this._map[Id] !== 'undefined').length === ids.length
if (!f1) {
throw new Error('collapse_wavefunction(): Unknown qubit id(s)'
+ ' provided. Try calling eng.flush() before '
+ 'invoking this function.')
}
let mask = 0
let val = 0
ids.forEach((looper, i) => {
const pos = this._map[looper]
mask |= (1 << pos)
val |= (Math.floor(values[i]) << pos)
})
let nrm = 0.0
this._state.forEach((looper, _i) => {
const i = _i[0]
if ((mask & i) === val) {
nrm += math.abs(this._getState(i)) ** 2
}
})
if (nrm < 1.e-12) {
throw new Error('collapse_wavefunction(): Invalid collapse! Probability is ~0.')
}
const inv_nrm = 1.0 / math.sqrt(nrm)
this._state.forEach((looper, _i) => {
const i = _i[0]
if ((mask & i) !== val) {
this._setState(i, 0)
} else {
this._setState(i, math.multiply(looper, inv_nrm))
}
})
}
}