QuEST is included into a C
or C++
project via
Simulation typically proceeds as:
- Initialise the QuEST environment, preparing available GPUs and networks.
- Configure the environment, such as through seeding.
- Create a
Qureg
, allocating memory for its amplitudes.
- Prepare its initial state, overwriting its amplitudes.
- Apply operators and decoherence, expressed as matrices and channels.
- Perform calculations, potentially using Pauli observables.
- Report or log the results to file.
- Destroy any heap-allocated
Qureg
or matrices.
- Finalise the QuEST environment.
Of course, the procedure is limited only by the programmers imagination ¯\_(ツ)_/¯
Let's see an example of these steps below.
TOC:
Initialise the environment
Before calling any other QuEST functions, we must initialise the QuEST environment.
This does several things, such as
- assessing which hardware accelerations (multithreading, GPU-acceleration, distribution, cuQuantum) were compiled and are currently available to use.
- initialising any external libraries as needed, like MPI, CUDA and cuQuantum.
- seeding the random number generators (informing measurements and random states), using a CSPRNG if available.
We could instead forcefully disable certain hardware accelerations
int useMPI = 0;
int useGPU = 0;
int useOMP = 0;
void initCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMultithread)
We can view the environment configuration at runtime, via
which might output something like
QuEST execution environment:
[precision]
qreal.................double (8 bytes)
qcomp.................std::__1::complex<double> (16 bytes)
qindex................long long int (8 bytes)
validationEpsilon.....1e-12
[compilation]
isMpiCompiled...........1
isGpuCompiled...........1
isOmpCompiled...........1
isCuQuantumCompiled.....0
[deployment]
isMpiEnabled.....0
isGpuEnabled.....1
isOmpEnabled.....1
[cpu]
numCpuCores.......10 per machine
numOmpProcs.......10 per machine
numOmpThrds.......8 per node
cpuMemory.........32 GiB per node
cpuMemoryFree.....7.1 GiB per node
[gpu]
numGpus...........1
gpuDirect.........1
gpuMemPools.......1
gpuMemory.........15.9 GiB per node
gpuMemoryFree.....15.2 GiB per node
gpuCache..........1 GiB
[distribution]
isMpiGpuAware.....0
numMpiNodes.......8
[statevector limits]
minQubitsForMpi.............3
maxQubitsForCpu.............30
maxQubitsForGpu.............29
maxQubitsForMpiCpu..........35
maxQubitsForMpiGpu..........34
maxQubitsForMemOverflow.....59
maxQubitsForIndOverflow.....63
[density matrix limits]
minQubitsForMpi.............2
maxQubitsForCpu.............15
maxQubitsForGpu.............14
maxQubitsForMpiCpu..........17
maxQubitsForMpiGpu..........16
maxQubitsForMemOverflow.....29
maxQubitsForIndOverflow.....31
[statevector autodeployment]
8 qubits.....[omp]
12 qubits....[gpu]
29 qubits....[gpu] [mpi]
[density matrix autodeployment]
4 qubits.....[omp]
6 qubits.....[gpu]
15 qubits....[gpu] [mpi]
We can also obtain some of the environment information programmatically
if (env.isGpuAccelerated)
printf("vroom vroom");
Configure the environment
Configuring the environment is ordinarily not necessary, but convenient in certain applications.
For example, we may wish our simulations to deterministically obtain the same measurement outcomes and random states as a previous or future run, and ergo choose to override the default seeds.
unsigned seeds[] = {123u, 1u << 10};
void setSeeds(unsigned *seeds, int numSeeds)
We may wish further to adjust how subsequent functions will display information to the screen
int maxRows = 8;
int maxCols = 4;
void setMaxNumReportedItems(qindex numRows, qindex numCols)
void setMaxNumReportedSigFigs(int numSigFigs)
or add extra spacing between QuEST's printed outputs
void setNumReportedNewlines(int numNewlines)
Perhaps we also wish to relax the precision with which our future inputs will be asserted unitary or Hermitian
void setValidationEpsilon(qreal eps)
but when unitarity is violated, or we otherwise pass an invalid input, we wish to execute a custom function before exiting.
#include <stdlib.h>
void myErrorHandler(const char *func, const char *msg) {
printf("QuEST function '%s' encountered error '%s'\n", func, msg);
printf("Exiting...\n");
exit(1);
}
void setInputErrorHandler(void(*callback)(const char *func, const char *msg))
Create a Qureg
To create a statevector of 10
qubits, we call
Qureg createQureg(int numQubits)
which we can verify has begun in the very boring zero state.
void reportQureg(Qureg qureg)
Qureg (10 qubit statevector, 1024 qcomps, 16.1 KiB):
1 |0⟩
0 |1⟩
0 |2⟩
0 |3⟩
â‹®
0 |1020⟩
0 |1021⟩
0 |1022⟩
0 |1023⟩
This printed only 8
amplitudes as per our setting of setMaxNumReportedItems()
above.
Behind the scenes, the function createQureg
did something clever; it consulted the compiled deployments and available hardware to decide whether to distribute qureg
, or dedicate it persistent GPU memory, and marked whether or not to multithread its subsequent modification. It attempts to choose optimally, avoiding gratuitous parallelisation if the overheads outweigh the benefits, or if the hardware devices have insufficient memory.
We call this auto-deployment, and the chosen configuration can be previewed via
void reportQuregParams(Qureg qureg)
Qureg:
[deployment]
isMpiEnabled.....0
isGpuEnabled.....0
isOmpEnabled.....1
[dimension]
isDensMatr.....0
numQubits......10
numCols........N/A
numAmps........2^10 = 1024
[distribution]
numNodes.....N/A
numCols......N/A
numAmps......N/A
[memory]
cpuAmps...........16 KiB
gpuAmps...........N/A
cpuCommBuffer.....N/A
gpuCommBuffer.....N/A
globalTotal.......16 KiB
The above output informs us that the qureg
has not been distributed nor GPU-accelerated, but will be multithreaded.
If we so wished, we could force the use of all deployments available to the environment
Qureg createForcedQureg(int numQubits)
Qureg:
[deployment]
isMpiEnabled.....1
isGpuEnabled.....1
isOmpEnabled.....1
[dimension]
isDensMatr.....0
numQubits......10
numCols........N/A
numAmps........2^10 = 1024
[distribution]
numNodes.....2^3 = 8
numCols......N/A
numAmps......2^7 = 128 per node
[memory]
cpuAmps...........2 KiB per node
gpuAmps...........2 KiB per node
cpuCommBuffer.....2 KiB per node
gpuCommBuffer.....2 KiB per node
globalTotal.......64 KiB
or select specific deployments
int useMPI = 1;
int useGPU = 0;
int useOMP = 0;
Qureg createCustomQureg(int numQubits, int isDensMatr, int useDistrib, int useGpuAccel, int useMultithread)
In lieu of a statevector, we could create a density matrix
Qureg createDensityQureg(int numQubits)
which is also auto-deployed. Note this contains square as many amplitudes as the equal-dimension statevector and ergo requires square as much memory.
Qureg (10 qubit density matrix, 1024x1024 qcomps, 16 MiB):
1 0 … 0 0
0 0 … 0 0
0 0 … 0 0
0 0 … 0 0
â‹®
0 0 … 0 0
0 0 … 0 0
0 0 … 0 0
0 0 … 0 0
Qureg:
...
[dimension]
isDensMatr.....1
numQubits......10
numCols........2^10 = 1024
numAmps........2^20 = 1048576
...
[memory]
cpuAmps...........16 MiB
...
globalTotal.......16 MiB
The spacing between the outputs of those two consecutive QuEST functions was determined by our earlier call to setMaxNumReportedSigFigs()
.
A density matrix Qureg
can model classical uncertainty as results from decoherence, and proves useful when simulating quantum operations on a noisy quantum computer.
Prepare an initial state
In lieu of manually modifying the state amplitudes, QuEST includes functions to prepare a Qureg
in some common initial states
void initPlusState(Qureg qureg)
void initZeroState(Qureg qureg)
void initPureState(Qureg qureg, Qureg pure)
void initClassicalState(Qureg qureg, qindex stateInd)
or random states
int numPureStates = 15;
void initRandomPureState(Qureg qureg)
void initRandomMixedState(Qureg qureg, qindex numPureStates)
Qureg (5 qubit statevector, 32 qcomps, 616 bytes):
0.0884-0.164i |0⟩
0.149+0.207i |1⟩
0.232+0.0656i |2⟩
-0.0435+0.0332i |3⟩
â‹®
-0.108-0.0431i |28⟩
-0.0161-0.121i |29⟩
-0.0463+0.00341i |30⟩
-0.0491-0.186i |31⟩
Qureg (5 qubit density matrix, 32x32 qcomps, 16.1 KiB):
0.0256+(1.08e-19)i -0.000876+0.00412i … 0.000912+0.00869i -0.00597+0.00615i
-0.000876-0.00412i 0.033-(6.78e-20)i … 0.000223+0.00369i -0.00207+0.00451i
-0.00443-0.00871i 0.0155-0.000843i … 0.00375+0.00669i (8.5e-5)-0.000851i
0.00287-0.00397i 0.00637-0.000315i … 0.00486+0.00218i 0.00268+0.0053i
â‹®
-0.00385-0.000732i 0.00965+0.00542i … 0.00162-0.0112i 0.00404+0.00685i
0.00491+0.00245i -0.000319+0.0021i … -0.00902-0.00312i -0.00465+0.00275i
0.000912-0.00869i 0.000223-0.00369i … 0.0183+(1.32e-19)i 0.000509+0.00401i
-0.00597-0.00615i -0.00207-0.00451i … 0.000509-0.00401i 0.0173+(3.12e-19)i
The number of printed significant figures above results from our earlier calling of setMaxNumReportedSigFigs()
.
Apply operators
QuEST supports an extensive set of operators to effect upon a Qureg
.
int target = 2;
qreal angle = 3.14 / 5;
int targets[] = {4,5,6};
void applyHadamard(Qureg qureg, int target)
void applyPhaseGadget(Qureg qureg, int *targets, int numTargets, qreal angle)
- Important
- Notice the type of
angle
is qreal
rather than the expected double
. This is a precision agnostic alias for a floating-point, real scalar which allows you to recompile QuEST with a varying precision with no modifications to your code.
controls
All unitary operations accept any number of control qubits
int controls[] = {0,1,2,3,7,8,9};
void applyMultiControlledSqrtSwap(Qureg qureg, int *controls, int numControls, int qubit1, int qubit2)
and even control states which specify the bits (0
or 1
) that the respective controls must be in to effect the non-identity operation.
int states[] = {0,0,0,1,1,1,0};
void applyMultiStateControlledRotateX(Qureg qureg, int *controls, int *states, int numControls, int target, qreal angle)
paulis
Some operators accept PauliStr
which can be constructed all sorts of ways - even inline!
void applyPauliGadget(Qureg qureg, PauliStr str, qreal angle)
PauliStr getPauliStr(const char *paulis, int *indices, int numPaulis)
matrices
CompMatr1
Don't see your operation in the API? You can specify it as a general matrix.
qcomp x = 1i/sqrt(2);
CompMatr1 matrix = getInlineCompMatr({{-x,x},{-x,-x}});
void applyCompMatr1(Qureg qureg, int target, CompMatr1 matrix)
- Important
- The type
qcomp
above is a precision agnostic complex scalar, and has beautiful arithmetic overloads! qcomp x = 1.5 + 3.14i;
qcomp *= 1E3i - 1E-5i;
Beware that in C++
, 1i
is a double precision literal, so C++
users should instead use the custom precision-agnostic literal 1_i
.
CompMatr
Want a bigger matrix? No problem - they can be any size, with many ways to initialise them.
CompMatr createCompMatr(int numQubits)
void setCompMatr(CompMatr matr, qcomp **vals)
void applyCompMatr(Qureg qureg, int *targets, int numTargets, CompMatr matr)
Matrix elements can be manually modified, though this requires we synchronise them with GPU memory once finished.
qindex dim = bigmatrix.numRows;
for (qindex r=0; r<dim; r++)
for (qindex c=0; c<dim; c++)
bigmatrix.cpuElems[r][c] = exp(rand() * 1i) * (r==c);
void syncCompMatr(CompMatr matr)
- Important
- The created
CompMatr
is a heap object and must be destroyed when we are finished with it, to free up its memory and avoid leaks.
void destroyCompMatr(CompMatr matrix)
This is true of any QuEST structure returned by a create*()
function. It is not true of functions prefixed with get*()
with are always stack variables, hence why functions like getCompMatr1()
can be called inline!
FullStateDiagMatr
Above, we initialised CompMatr
to a diagonal unitary. This is incredibly wasteful; only 256
of its 65536
elements are non-zero! We should instead use DiagMatr
or FullStateDiagMatr
. The latter is even distributed (if chosen by the autodeployer), permitting it to be as large as a Qureg
itself!
FullStateDiagMatr createFullStateDiagMatr(int numQubits)
and can be initialised in many ways, including from all-Z
pauli sums!
1 II
1i ZI
1i IZ
-1 ZZ
)");
void setFullStateDiagMatrFromPauliStrSum(FullStateDiagMatr out, PauliStrSum in)
PauliStrSum createInlinePauliStrSum(const char *str)
- Important
- The argument to
createInlinePauliStrSum
is a multiline string for which the syntax differs between C
and C++
; we used the latter above. See examples initialisation.c
and initialisation.cpp
for clarity.
- Attention
- Beware that in distributed settings, because
fullmatrix
may be distributed, we should must exercise extreme caution when modifying its fullmatrix.cpuElems
directly.
A FullStateDiagMatr
acts upon all qubits of a qureg
void applyFullStateDiagMatr(Qureg qureg, FullStateDiagMatr matrix)
and can be raised to an arbitrary power, helpful for example in simulating quantum spectral methods.
qcomp exponent = 3.5;
void applyFullStateDiagMatrPower(Qureg qureg, FullStateDiagMatr matrix, qcomp exponent)
Notice the exponent
is a qcomp
and ergo permitted to be a complex number. Unitarity requires exponent
is strictly real, but we can always relax the unitarity validation...
validation
Our example above initialised CompMatr
to a diagonal because it is tricky to generate random non-diagonal unitary matrices - and QuEST checks for unitarity!
static CompMatr1 getCompMatr1(qcomp **in)
QuEST encountered a validation error during function 'applyCompMatr1':
The given matrix was not (approximately) unitary.
Exiting...
If we're satisfied our matrix is sufficiently approximately unitary, we can adjust or disable the validation.
circuits
QuEST includes a few convenience functions for effecting QFT and Trotter circuits.
qreal time = .3;
int order = 4;
int reps = 10;
void applyTrotterizedPauliStrSumGadget(Qureg qureg, PauliStrSum sum, qreal angle, int order, int reps)
void applyQuantumFourierTransform(Qureg qureg, int *targets, int numTargets)
measurements
We can also effect a wide range of non-unitary operations, such as destructive measurements
qreal prob;
int applyQubitMeasurement(Qureg qureg, int target)
qindex applyMultiQubitMeasurementAndGetProb(Qureg qureg, int *qubits, int numQubits, qreal *probability)
and conveniently report their outcome.
void reportScalar(const char *label, qcomp num)
- Important
- Notice the type of
outcome2
is a qindex
rather than an int
. This is a larger type which can store much larger numbers without overflow - up to 2^63
- and is always used by the API for many-qubit indices.
Should we wish to leave the state unnormalised, we can instead use projectors.
decoherence
Density matrices created with createDensityQureg()
can undergo decoherence channels.
qreal prob = 0.1;
void mixDamping(Qureg qureg, int qubit, qreal prob)
void mixTwoQubitDepolarising(Qureg qureg, int qubit1, int qubit2, qreal prob)
void mixDephasing(Qureg qureg, int qubit, qreal prob)
which we can specify as inhomogeneous Pauli channels
void mixPaulis(Qureg qureg, int qubit, qreal probX, qreal probY, qreal probZ)
or completely generally as Kraus maps and superoperators!
int numTargets = 1;
int numOperators = 4;
qreal p = 0.1;
qreal l = 0.3;
{
{sqrt(p), 0},
{0, sqrt(p*(1-l))}
}, {
{0, sqrt(p*l)},
{0, 0}
}, {
{sqrt((1-p)*(1-l)), 0},
{0, sqrt(1-p)}
}, {
{0, 0},
{sqrt((1-p)*l), 0}
}
});
int victims[] = {2};
KrausMap createInlineKrausMap(int numQubits, int numOperators, std::vector< std::vector< std::vector< qcomp > > > matrices)
void mixKrausMap(Qureg qureg, int *targets, int numTargets, KrausMap map)
We can even directy mix density matrices together
void mixQureg(Qureg qureg, Qureg other, qreal prob)
Sometimes we wish to left-multiply general operators upon density matrices without also right-multiplying their adjoint - i.e. our operators should not be effected as unitaries. We can do this with the multiply*()
functions.
void multiplyDiagMatrPower(Qureg qureg, int *targets, int numTargets, DiagMatr matrix, qcomp exponent)
Perform calculations
After so much modification to our state, we will find that its amplitudes have differed substantially. But it's impractical to observe the exponentially-many amplitudes with reportQureg()
. We can instead give QuEST the questions we wish to answer about the resulting state.
For example, we can find the probability of measurement outcomes without modifying the state.
int outcome = 1;
int qubits[] = {2,3,4};
int outcomes[] = {0,1,1};
qreal calcProbOfQubitOutcome(Qureg qureg, int qubit, int outcome)
qreal calcProbOfMultiQubitOutcome(Qureg qureg, int *qubits, int *outcomes, int numQubits)
We can obtain all outcome probabilities in one swoop:
qreal probs[8];
void calcProbsOfAllMultiQubitOutcomes(qreal *outcomeProbs, Qureg qureg, int *qubits, int numQubits)
It is similarly trivial to find expectation values
qreal calcExpecFullStateDiagMatr(Qureg qureg, FullStateDiagMatr matr)
qreal calcExpecPauliStrSum(Qureg qureg, PauliStrSum sum)
qreal calcExpecPauliStr(Qureg qureg, PauliStr str)
or distance measures between states, including between statevectors and density matrices.
qreal calcFidelity(Qureg qureg, Qureg other)
qreal calcDistance(Qureg qureg1, Qureg qureg2)
qreal calcPurity(Qureg qureg)
We can even find reduced density matrices resulting from partially tracing out qubits.
Qureg calcPartialTrace(Qureg qureg, int *traceOutQubits, int numTraceQubits)
Report the results
We've seen above that scalars can be reported, handling the pretty formatting of real and complex numbers, controlled by settings like setMaxNumReportedSigFigs()
. But we can also report every data structure in the QuEST API, such as Pauli strings
);
PauliStr getInlinePauliStr(const char *paulis, { list })
void reportPauliStr(PauliStr str)
YIIIIIIIIIXIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIYIIIIXIIIII
and their weighted sums
void reportPauliStrSum(PauliStrSum str)
PauliStrSum (4 terms, 160 bytes):
1 II
i ZI
i IZ
-1 ZZ
All outputs are affected by the reporter settings.
void reportCompMatr(CompMatr matrix)
CompMatr (8 qubits, 256x256 qcomps, 1 MiB):
0.9-0.5i 0 … 0 0
0 0.8-0.6i … 0 0
â‹®
0 0 … -0.5-0.9i 0
0 0 … 0 0.4+0.9i
- Note
- Facilities for automatically logging to file are coming soon!
Cleanup
While not strictly necessary before the program ends, it is a good habit to destroy data structures as soon as you are finished with them, freeing their memory.
void destroyKrausMap(KrausMap map)
void destroyFullStateDiagMatr(FullStateDiagMatr matrix)
void destroyPauliStrSum(PauliStrSum sum)
void destroyQureg(Qureg qureg)
Finalise QuEST
The final step of our program should be to call
which ensures everything is synchronised, frees accelerator resources, and finalises MPI. This is important because it ensures:
- everything is done, and that distributed nodes that are still working (e.g. haven't yet logged to their own file) are not interrupted by early termination of another node.
- the MPI process ends gracefully, and doesn't spew out messy errors!
- our GPU processes are killed quickly, freeing resources for other processes.
- Attention
- After calling
finalizeQuESTEnv()
, MPI will close and each if being accessed directly by the user, will enter an undefined state. Subsequent calls to MPI routines may return gibberish, and distributed machines will have lost their ability to communicate. It is recommended to call finalizeQuESTEnv()
immediately before exiting.
You are now a QuEST expert 🎉 though there are many more functions in the API not covered here. Go forth and simulate!