├── README.md ├── catch ├── catch_amalgamated.cpp └── catch_amalgamated.hpp ├── compile.sh ├── main.cpp ├── src ├── bit_maths.hpp ├── communication.hpp ├── distributed_densitymatrix.hpp ├── distributed_statevector.hpp ├── local_densitymatrix.hpp ├── local_statevector.hpp ├── misc.hpp ├── states.hpp └── types.hpp └── tests ├── test_utilities.hpp ├── tests.cpp ├── tests_densitymatrix.hpp └── tests_statevector.hpp /README.md: -------------------------------------------------------------------------------- 1 | 2 | [Distributed Simulation of Statevectors and Density Matrices](https://arxiv.org/abs/2311.01512) 3 | =========================================================== 4 | 5 | > Tyson Jones, Balint Koczor, Simon C. Benjamin 6 | > - Department of Materials, University of Oxford 7 | > - Quantum Motion Technologies Ltd 8 | 9 | 10 | This repository contains `C++` implementations of the multithreaded, distributed algorithms presented in [this manuscript](https://arxiv.org/abs/2311.01512), and unit tests using [Catch2](https://github.com/catchorg/Catch2). If the code is useful to you, feel free to cite 11 | ``` 12 | @misc{jones2023distributed, 13 | title={Distributed Simulation of Statevectors and Density Matrices}, 14 | author={Tyson Jones and Bálint Koczor and Simon C. Benjamin}, 15 | year={2023}, 16 | eprint={2311.01512}, 17 | archivePrefix={arXiv}, 18 | primaryClass={quant-ph} 19 | } 20 | ``` 21 | 22 | # Types 23 | 24 | The below API makes use of the following custom types defined in [`types.hpp`](src/types.hpp), wherein you can vary their precision. 25 | 26 | | Type | Use | Default 27 | |--------|---------------|----------| 28 | | `Real` | A real scalar | `double` | 29 | | `Nat` | A natural scalar | `unsigned int` | 30 | | `Index` | A state index | `long long unsigned int` | 31 | | `Amp` | A complex scalar | `std::complex` | 32 | 33 | We also define arrays and matrices of these types, such as `NatArray`, which are merely eye-candy for `std::vector`. 34 | 35 | 36 | # API 37 | 38 | Before calling any of the below functions, you should initialise MPI with `comm_init()`, and before exiting, finalise with `comm_end()`. 39 | 40 | Instantiate a quantum state via: 41 | ```C++ 42 | StateVector psi = StateVector(numQubits); 43 | DensityMatrix rho = DensityMatrix(numQubits); 44 | ``` 45 | 46 | Statevectors can be passed to the below unitary functions, prefixed with `distributed_statevector_`. 47 | 48 | - ```C++ 49 | oneTargGate(StateVector psi, Nat target, AmpMatrix gate) 50 | ``` 51 | - ```C++ 52 | manyCtrlOneTargGate(StateVector psi, NatArray controls, Nat target, AmpMatrix gate) 53 | ``` 54 | - ```C++ 55 | swapGate(StateVector psi, Nat qb1, Nat qb2) 56 | ``` 57 | - ```C++ 58 | manyTargGate(StateVector psi, NatArray targets, AmpMatrix gate) 59 | ``` 60 | - ```C++ 61 | pauliTensor(StateVector psi, NatArray targets, NatArray paulis) 62 | ``` 63 | - ```C++ 64 | pauliGadget(StateVector psi, NatArray targets, NatArray paulis, Real theta) 65 | ``` 66 | - ```C++ 67 | phaseGadget(StateVector psi, NatArray targets, Real theta) 68 | ``` 69 | 70 | Density matrices can be passed to the below functions, prefixed with `distributed_densitymatrix_`. 71 | 72 | - ```C++ 73 | manyTargGate(DensityMatrix rho, NatArray targets, AmpMatrix gate) 74 | ``` 75 | - ```C++ 76 | swapGate(DensityMatrix rho, Nat qb1, Nat qb2) 77 | ``` 78 | - ```C++ 79 | pauliTensor(DensityMatrix rho, NatArray targets, NatArray paulis) 80 | ``` 81 | - ```C++ 82 | pauliGadget(DensityMatrix rho, NatArray targets, NatArray paulis, Real theta) 83 | ``` 84 | - ```C++ 85 | phaseGadget(DensityMatrix rho, NatArray targets, Real theta) 86 | ``` 87 | - ```C++ 88 | phaseGadget(DensityMatrix rho, NatArray targets, Real theta) 89 | ``` 90 | - ```C++ 91 | oneQubitDephasing(DensityMatrix rho, Nat qb, Real prob) 92 | ``` 93 | - ```C++ 94 | twoQubitDephasing(DensityMatrix rho, Nat qb1, Nat qb2, Real prob) 95 | ``` 96 | - ```C++ 97 | oneQubitDepolarising(DensityMatrix rho, Nat qb, Real prob) 98 | ``` 99 | - ```C++ 100 | twoQubitDepolarising(DensityMatrix rho, Nat qb1, Nat qb2, Real prob) 101 | ``` 102 | - ```C++ 103 | damping(DensityMatrix rho, Nat qb, Real prob) 104 | ``` 105 | - ```C++ 106 | expecPauliString(DensityMatrix rho, RealArray coeffs, NatArray allPaulis) 107 | ``` 108 | - ```C++ 109 | partialTrace(DensityMatrix inRho, NatArray targets) 110 | ``` 111 | 112 | View the definition of these functions in the [`src`](/src/) folder. 113 | 114 | See an example in [`main.cpp`](main.cpp). 115 | 116 | 117 | # Compiling 118 | 119 | To compile both [`main.cpp`](main.cpp) and the [unit tests](/tests/), simply call 120 | ```bash 121 | source ./compile 122 | ``` 123 | Additionally, set the number of threads (per node) via 124 | ```bash 125 | export OMP_NUM_THREADS=24 126 | ``` 127 | and launch the executables between (e.g.) `16` nodes via 128 | ```bash 129 | mpirun -np 16 ./main 130 | ``` 131 | ```bash 132 | mpirun -np 16 ./test 133 | ``` 134 | You must use a power-of-2 number of nodes. 135 | 136 | 137 | # License 138 | 139 | This repository is licensed under the terms of the MIT license. -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | 2 | conf="-std=c++17 -O3 -fopenmp -march=native" 3 | dirs="-Isrc -Itests -Icatch" 4 | 5 | echo "compiling main..." 6 | mpic++ $conf $dirs main.cpp -o main $* 7 | 8 | echo "compiling tests..." 9 | mpic++ $conf $dirs tests/tests.cpp catch/catch_amalgamated.cpp -o test $* 10 | 11 | export OMP_WAIT_POLICY=active 12 | export OMP_DYNAMIC=false 13 | export OMP_PROC_BIND=true 14 | export OMP_NUM_THREADS=24 # set to #cores per node -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "types.hpp" 2 | #include "states.hpp" 3 | #include "local_statevector.hpp" 4 | #include "distributed_statevector.hpp" 5 | #include "distributed_densitymatrix.hpp" 6 | #include "test_utilities.hpp" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std::chrono; 14 | using namespace std::complex_literals; 15 | 16 | 17 | 18 | int main() { 19 | 20 | comm_init(); 21 | 22 | Nat numQubits = 26; 23 | StateVector state = StateVector(numQubits); 24 | 25 | NatArray targets = {0,6,4,2}; 26 | AmpMatrix matrix = getRandomMatrix( powerOf2(targets.size()) ); 27 | 28 | auto start = high_resolution_clock::now(); 29 | comm_synch(); 30 | 31 | distributed_statevector_manyTargGate(state, targets, matrix); 32 | 33 | comm_synch(); 34 | auto stop = high_resolution_clock::now(); 35 | auto dur = duration_cast(stop - start).count(); 36 | 37 | rootNodePrint("done in " + std::to_string(dur) + " microseconds\n"); 38 | 39 | comm_end(); 40 | return 0; 41 | } -------------------------------------------------------------------------------- /src/bit_maths.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BIT_MATHS_HPP 2 | #define BIT_MATHS_HPP 3 | 4 | 5 | #include "types.hpp" 6 | 7 | 8 | 9 | /* 10 | * Fast and inlined; safe to call in tight loops. 11 | * None of these twiddles may use sign tricks, 12 | * since Nat and Index are unsigned. 13 | */ 14 | 15 | 16 | #define INLINE static inline __attribute__((always_inline)) 17 | 18 | 19 | INLINE Index powerOf2(Nat exponent) { 20 | 21 | return 1ULL << exponent; 22 | } 23 | 24 | 25 | INLINE bool isPowerOf2(Index number) { 26 | 27 | return (number > 0) && ((number & (number - 1U)) == 0); 28 | } 29 | 30 | 31 | INLINE Nat getBit(Index number, Nat bitIndex) { 32 | 33 | return (number >> bitIndex) & 1U; 34 | } 35 | 36 | 37 | INLINE Index flipBit(Index number, Nat bitIndex) { 38 | 39 | return number ^ (1ULL << bitIndex); 40 | } 41 | 42 | 43 | INLINE Index insertBit(Index number, Nat bitIndex, Nat bitValue) { 44 | 45 | Index left = (number >> bitIndex) << (bitIndex + 1); 46 | Index middle = bitValue << bitIndex; 47 | Index right = number & ((1ULL << bitIndex) - 1); 48 | return left | middle | right; 49 | } 50 | 51 | 52 | INLINE Index insertBits(Index number, NatArray bitIndices, Nat bitValue) { 53 | 54 | // bitIndices must be strictly increasing 55 | for (Nat i: bitIndices) 56 | number = insertBit(number, i, bitValue); 57 | 58 | return number; 59 | } 60 | 61 | 62 | INLINE Index setBit(Index number, Nat bitIndex, Nat bitValue) { 63 | 64 | Index comp = ~ (1ULL << bitIndex); 65 | Index mask = bitValue << bitIndex; 66 | return (number & (~comp)) | mask; 67 | } 68 | 69 | 70 | INLINE Index setBits(Index number, NatArray bitIndices, Index bitsValue) { 71 | 72 | for (Nat i=0; i>= 1; 170 | } 171 | 172 | return expo; 173 | } 174 | 175 | 176 | #endif // BIT_MATHS_HPP 177 | -------------------------------------------------------------------------------- /src/communication.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMMUNICATION_HPP 2 | #define COMMUNICATION_HPP 3 | 4 | 5 | #include "types.hpp" 6 | #include "bit_maths.hpp" 7 | 8 | #include 9 | 10 | 11 | 12 | /* 13 | * MPI environment management 14 | */ 15 | 16 | static void comm_init() { 17 | int isInit; 18 | MPI_Initialized(&isInit); 19 | if (!isInit) 20 | MPI_Init(NULL, NULL); 21 | } 22 | 23 | 24 | static void comm_end() { 25 | MPI_Barrier(MPI_COMM_WORLD); 26 | MPI_Finalize(); 27 | } 28 | 29 | 30 | static Nat comm_getRank() { 31 | int rank; 32 | MPI_Comm_rank(MPI_COMM_WORLD, &rank); 33 | return (Nat) rank; 34 | } 35 | 36 | 37 | static Nat comm_getNumNodes() { 38 | int numNodes; 39 | MPI_Comm_size(MPI_COMM_WORLD, &numNodes); 40 | return (Nat) numNodes; 41 | } 42 | 43 | 44 | static void comm_synch() { 45 | 46 | MPI_Barrier(MPI_COMM_WORLD); 47 | } 48 | 49 | 50 | /* 51 | * internal convenience functions 52 | */ 53 | 54 | static int NULL_TAG = 0; 55 | 56 | static void getMessageConfig(Index *messageSize, Index *numMessages, Index numAmps) { 57 | 58 | assert( isPowerOf2(numAmps) ); 59 | 60 | // determine the number of max-size messages 61 | *messageSize = powerOf2(30); 62 | *numMessages = numAmps / *messageSize; // divides evenly 63 | 64 | // when numAmps < messageSize, we need send only one smaller message (obviously) 65 | if (*numMessages == 0) { 66 | *messageSize = numAmps; 67 | *numMessages = 1; 68 | } 69 | } 70 | 71 | 72 | 73 | /* 74 | * synchronous amplitude exchange 75 | */ 76 | 77 | static void comm_exchangeInChunks(Amp* send, Amp* recv, Index numAmps, Nat pairRank) { 78 | 79 | // each message is asynchronously dispatched with a final wait, as per arxiv.org/abs/2308.07402 80 | 81 | // divide the data into multiple messages 82 | Index messageSize, numMessages; 83 | getMessageConfig(&messageSize, &numMessages, numAmps); 84 | 85 | // each asynch message below will create two requests for subsequent synch 86 | std::vector requests(2*numMessages); 87 | 88 | // asynchronously exchange the messages (effecting MPI_Isendrecv), exploiting orderedness gaurantee 89 | for (Index m=0; m requests(numMessages); 151 | 152 | // listen to receive each message asynchronously (as per arxiv.org/abs/2308.07402) 153 | for (Index m=0; m qb2) 245 | std::swap(qb1, qb2); 246 | 247 | Amp c1 = 1 - 4*prob/5; 248 | Amp c2 = 4*prob/15; 249 | Amp c3 = -16*prob/15; 250 | Nat shift1 = qb1 + rho.numQubits; 251 | Nat shift2 = qb2 + rho.numQubits; 252 | 253 | Nat threshold = rho.numQubits - rho.logNumNodes; 254 | 255 | if (qb2 < threshold) 256 | local_densitymatrix_twoQubitDepolarising(rho, qb1, qb2, shift1, shift2, c1, c2, c3); 257 | 258 | else if (qb2 >= threshold && qb1 < threshold) 259 | distributed_densitymatrix_twoQubitDepolarising_subroutine_pair(rho, qb1, qb2, shift1, c1, c2, c3); 260 | 261 | else 262 | distributed_densitymatrix_twoQubitDepolarising_subroutine_quad(rho, qb1, qb2, c1, c2, c3); 263 | } 264 | 265 | 266 | static void distributed_densitymatrix_damping(DensityMatrix &rho, Nat qb, Real prob) { 267 | 268 | Nat threshold = rho.numQubits - rho.logNumNodes; 269 | 270 | if (qb < threshold) 271 | local_densitymatrix_damping(rho, qb, prob); 272 | 273 | else { 274 | Index numIts = rho.numAmpsPerNode / 2; 275 | Nat pairRank = flipBit(rho.rank, qb - threshold); 276 | Nat bit = getBit(rho.rank, qb - threshold); 277 | Amp c1 = sqrt(1 - prob); 278 | Amp c2 = 1 - prob; 279 | 280 | // half of all nodes... 281 | if (bit == 1) { 282 | 283 | // pack half buffer and scale half their amps 284 | #pragma omp parallel for 285 | for (Index k=0; k= inRho.logNumNodes ); 353 | 354 | /* Note this algorithm imposes a looser constraint; that 355 | * targets.size() <= inRho.numQubits - (inRho.logNumNodes/2) 356 | * and ergo permits tracing out more qubits than the above restriction 357 | * permits. However, the reduced density matrices violate the precondition 358 | * of the DensityMatrix constructor 359 | */ 360 | 361 | // targets must be sorted for bitwise insertions 362 | std::sort(targets.begin(), targets.end()); 363 | 364 | // if all targets are in suffix, invoke embarrassingly parallel trace on {t, t+N}, and return 365 | if (targets.back() + inRho.numQubits < inRho.logNumAmpsPerNode) { 366 | NatArray pairs = targets; 367 | for (Nat &t : pairs) 368 | t += inRho.numQubits; 369 | return local_densitymatrix_partialTrace(inRho, targets, pairs); 370 | } 371 | 372 | // otherwise, treating outRho as a statevector, collect all involved qubits... 373 | NatArray extendedTargets = targets; 374 | for (Nat t : targets) 375 | extendedTargets.push_back(t + inRho.numQubits); 376 | 377 | // and find which qubits they should be swapped with, so that all effective targets lie in the suffix substate 378 | NatArray reorderedTargets = getReorderedAllSuffixTargets(extendedTargets, inRho.logNumAmpsPerNode); 379 | 380 | // effect those swaps, enabling subsequent (disordered) partial trace to be embarrassingly parallel. 381 | // we iterate in reverse (swap leftmost qubits first), to minimise violating relative order of non-targeted bits 382 | for (Nat q = reorderedTargets.size(); q-- != 0; ) 383 | if (reorderedTargets[q] != extendedTargets[q]) 384 | distributed_statevector_swapGate(inRho, reorderedTargets[q], extendedTargets[q]); 385 | 386 | // embarassingly parallel reduce the density matrix 387 | NatArray pairTargets(reorderedTargets.begin() + targets.size(), reorderedTargets.end()); 388 | DensityMatrix outRho = local_densitymatrix_partialTrace(inRho, targets, pairTargets); 389 | 390 | // determine the relative ordering of the remaining non-targered qubits 391 | NatArray remainingQubits = getNonTargetedQubitOrder(2*inRho.numQubits, extendedTargets, reorderedTargets); 392 | 393 | // perform additional swaps to re-order the remaining qubits 394 | for (Nat q=remainingQubits.size(); q-- != 0; ) { 395 | if (remainingQubits[q] == q) 396 | continue; 397 | Nat p = 0; 398 | while (remainingQubits[p] != q) 399 | p++; 400 | 401 | distributed_statevector_swapGate(outRho, q, p); 402 | std::swap(remainingQubits[q], remainingQubits[p]); 403 | } 404 | 405 | return outRho; 406 | } 407 | 408 | 409 | #endif // DISTRIBUTED_DENSITYMATRIX_HPP -------------------------------------------------------------------------------- /src/distributed_statevector.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DISTRIBUTED_STATEVECTOR_HPP 2 | #define DISTRIBUTED_STATEVECTOR_HPP 3 | 4 | 5 | #include "types.hpp" 6 | #include "states.hpp" 7 | #include "bit_maths.hpp" 8 | #include "misc.hpp" 9 | #include "communication.hpp" 10 | 11 | #include "local_statevector.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | void distributed_statevector_oneTargGate(StateVector& psi, Nat target, AmpMatrix gate) { 19 | 20 | // embarrassingly parallel 21 | if (target < psi.logNumAmpsPerNode) 22 | local_statevector_oneTargGate(psi, target, gate); 23 | 24 | else { 25 | // exchange all amps (receive to buffer) 26 | Nat rankTarget = target - psi.logNumAmpsPerNode; 27 | Nat pairRank = flipBit(psi.rank, rankTarget); 28 | comm_exchangeArrays(psi.amps, psi.buffer, pairRank); 29 | 30 | // extract relevant gate elements 31 | Nat bit = getBit(psi.rank, rankTarget); 32 | Amp fac0 = gate[bit][bit]; 33 | Amp fac1 = gate[bit][!bit]; 34 | 35 | // update psi using local and received amps 36 | #pragma omp parallel for 37 | for (Index i=0; i 0, it's gauranteed that numAmpsToMod < logNumAmpsPerNode/2 53 | Index numAmpsToMod = psi.numAmpsPerNode / powerOf2(controls.size()); 54 | 55 | // pack sub-buffer[0...] 56 | #pragma omp parallel for 57 | for (Index j=0; j= psi.logNumAmpsPerNode) 87 | prefixCtrls.push_back(q - psi.logNumAmpsPerNode); 88 | else 89 | suffixCtrls.push_back(q); 90 | 91 | // do nothing if this node fails prefix control condition 92 | if (!allBitsAreOne(psi.rank, prefixCtrls)) 93 | return; 94 | 95 | // embarrassingly parallel 96 | if (target < psi.logNumAmpsPerNode) 97 | local_statevector_manyCtrlOneTargGate(psi, suffixCtrls, target, gate); 98 | 99 | // no suffix controls; effect non-controlled gate 100 | else if (suffixCtrls.size() == 0) 101 | distributed_statevector_oneTargGate(psi, target, gate); 102 | 103 | // bespoke communication for controls required 104 | else 105 | distributed_statevector_manyCtrlOneTargGate_subroutine(psi, suffixCtrls, target, gate); 106 | } 107 | 108 | 109 | static void distributed_statevector_swapGate(StateVector &psi, Nat qb1, Nat qb2) { 110 | 111 | // ensure qb2 is larger 112 | if (qb1 > qb2) 113 | std::swap(qb1, qb2); 114 | 115 | // embarrassingly parallel 116 | if (qb2 < psi.logNumAmpsPerNode) 117 | local_statevector_swapGate(psi, qb1, qb2); 118 | 119 | // zero or one full-statevector swaps 120 | else if (qb1 >= psi.logNumAmpsPerNode) { 121 | Nat alt1 = qb1 - psi.logNumAmpsPerNode; 122 | Nat alt2 = qb2 - psi.logNumAmpsPerNode; 123 | 124 | // half of the nodes do nothing, the other half swap 125 | if (getBit(psi.rank, alt1) != getBit(psi.rank, alt2)) { 126 | 127 | Nat pairRank = flipBit(psi.rank, alt1); 128 | pairRank = flipBit(pairRank, alt2); 129 | 130 | // directly swap amps (although MPI arrays must not overlap) 131 | comm_exchangeArrays(psi.amps, psi.buffer, pairRank); 132 | 133 | #pragma omp parallel for 134 | for (Index j=0; j= psi.logNumAmpsPerNode) 257 | if (paulis[i] == X || paulis[i] == Y) 258 | pairRank = flipBit(pairRank, targets[i] - psi.logNumAmpsPerNode); 259 | 260 | NatArray suffixTargsXY(0); 261 | for (Nat i=0; i 10 | 11 | 12 | static void local_densitymatrix_oneQubitDephasing(DensityMatrix &rho, Nat qb, Real prob) { 13 | 14 | Amp fac = 1 - 2*prob; 15 | 16 | if (qb >= rho.numQubits - rho.logNumNodes) { 17 | 18 | Index numIts = rho.numAmpsPerNode / 2; 19 | Nat bit = ! getBit(rho.rank, qb - (rho.numQubits - rho.logNumNodes)); 20 | 21 | #pragma omp parallel for 22 | for (Index k=0; k 11 | #include 12 | 13 | 14 | static void local_statevector_oneTargGate(StateVector& psi, Nat target, AmpMatrix gate) { 15 | 16 | Index numIts = psi.numAmpsPerNode / 2; 17 | 18 | #pragma omp parallel for 19 | for (Index j=0; j qb2) 58 | std::swap(qb1, qb2); 59 | 60 | Index numIts = psi.numAmpsPerNode / 4; 61 | 62 | #pragma omp parallel for 63 | for (Index k=0; k 6 | 7 | #include "bit_maths.hpp" 8 | #include "types.hpp" 9 | 10 | 11 | 12 | /* 13 | * Relatively fast compared to caller; can be used in tight-loops 14 | */ 15 | 16 | INLINE Amp getPauliTensorElem(Nat* pauliCodes, Nat numQubits, Index flatInd) { 17 | 18 | AmpMatrix I{{1,0},{0,1}}; 19 | AmpMatrix X{{0,1},{1,0}}; 20 | AmpMatrix Y{{0,Amp(0,-1)},{Amp(0,1),0}}; 21 | AmpMatrix Z{{1,0},{0,-1}}; 22 | MatrixArray pauliMatrices{ I, X, Y, Z }; 23 | 24 | Amp elem = 1.; 25 | 26 | for (Nat q=0; q 10 | 11 | 12 | 13 | class StateVector { 14 | 15 | public: 16 | Nat rank; 17 | Nat numNodes; 18 | Nat logNumNodes; 19 | 20 | Nat numQubits; 21 | Index numAmpsPerNode; 22 | Index logNumAmpsPerNode; 23 | 24 | AmpArray amps; 25 | AmpArray buffer; 26 | 27 | AmpArray getAllVecAmps(); 28 | void setRandomAmps(); 29 | void printAmps(); 30 | bool agreesWith(AmpArray allAmps, Real tol); 31 | 32 | StateVector(Nat numQubits) { 33 | 34 | // enforces >=1 statevec amp per node, and >=1 density-matrix column per node 35 | assert( powerOf2(numQubits) >= comm_getNumNodes() ); 36 | 37 | this->rank = comm_getRank(); 38 | this->numNodes = comm_getNumNodes(); 39 | 40 | this->numQubits = numQubits; 41 | this->logNumNodes = logBase2(this->numNodes); 42 | 43 | this->logNumAmpsPerNode = (numQubits - this->logNumNodes); 44 | this->numAmpsPerNode = powerOf2(this->logNumAmpsPerNode); 45 | 46 | this->amps = AmpArray(this->numAmpsPerNode, 0); 47 | this->buffer = AmpArray(this->numAmpsPerNode, 0); 48 | } 49 | }; 50 | 51 | 52 | 53 | class DensityMatrix : public StateVector { 54 | 55 | public: 56 | 57 | AmpMatrix getAllMatrAmps(); 58 | bool agreesWith(AmpMatrix allAmps, Real tol); 59 | 60 | DensityMatrix(Nat numQubits) : StateVector(numQubits) { 61 | 62 | // increase the number of initialised statevector amps 63 | this->logNumAmpsPerNode = (2*numQubits - this->logNumNodes); 64 | this->numAmpsPerNode = powerOf2(this->logNumAmpsPerNode); 65 | 66 | this->amps.resize(this->numAmpsPerNode, 0); 67 | this->buffer.resize(this->numAmpsPerNode, 0); 68 | } 69 | }; 70 | 71 | 72 | 73 | #endif // STATES_HPP -------------------------------------------------------------------------------- /src/types.hpp: -------------------------------------------------------------------------------- 1 | 2 | #ifndef TYPES_HPP 3 | #define TYPES_HPP 4 | 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | 12 | /* 13 | * interface for specifying Pauli tensors and gadgets 14 | */ 15 | 16 | enum PauliOperator { 17 | I, X, Y, Z 18 | }; 19 | 20 | 21 | 22 | /* 23 | * numerical precision config 24 | */ 25 | 26 | typedef double Real; 27 | typedef unsigned int Nat; 28 | typedef long long unsigned int Index; 29 | #define MPI_AMP MPI_DOUBLE_COMPLEX 30 | 31 | 32 | 33 | /* 34 | * local complex, array, matrix convenience types 35 | */ 36 | 37 | typedef std::complex Amp; 38 | typedef std::vector AmpArray; 39 | typedef std::vector AmpMatrix; 40 | typedef std::vector NatArray; 41 | typedef std::vector MatrixArray; 42 | typedef std::vector RealArray; 43 | 44 | 45 | 46 | /* 47 | * inform OpenMP how to reduce Amp 48 | */ 49 | 50 | #pragma omp declare reduction(+ : Amp : omp_out += omp_in ) initializer( omp_priv = omp_orig ) 51 | 52 | 53 | 54 | /* 55 | * operator overloads for array and matrix 56 | */ 57 | 58 | static AmpMatrix getZeroMatrix(Index dim) { 59 | assert( dim > 0 ); 60 | 61 | AmpMatrix matrix = AmpMatrix(dim); 62 | for (Index r=0; r 0 ); 70 | 71 | AmpMatrix matrix = getZeroMatrix(dim); 72 | for (Index r=0; r 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | #include "catch_amalgamated.hpp" 17 | 18 | 19 | Real PI = 3.14159265358979323846; 20 | 21 | AmpMatrix matrI = {{1,0}, {0,1}}; 22 | AmpMatrix matrX = {{0,1}, {1,0}}; 23 | AmpMatrix matrY = {{0,Amp(0,-1)}, {Amp(0,1),0}}; 24 | AmpMatrix matrZ = {{1,0}, {0,-1}}; 25 | 26 | 27 | static void printMatrix(AmpMatrix matrix) { 28 | for (Nat r=0; r 37 | static void printArray(std::vector array) { 38 | std::cout << "{"; 39 | for (Nat i=0; i 56 | static void allNodesPrintLocalArray(std::vector array) { 57 | Nat thisRank = comm_getRank(); 58 | Nat numNodes = comm_getNumNodes(); 59 | for (Nat rank=0; rank 73 | static void rootNodePrintLocalArray(std::vector array) { 74 | comm_synch(); 75 | if (comm_getRank() == 0) 76 | printArray(array); 77 | comm_synch(); 78 | std::cout << std::flush; 79 | comm_synch(); 80 | } 81 | 82 | 83 | static Nat getRandomNat(Nat minInclusive, Nat maxExclusive) { 84 | assert( maxExclusive > minInclusive ); 85 | 86 | // unimportantly uniform (and MPI-safe) 87 | Nat n = maxExclusive - minInclusive; 88 | Nat r = (Nat) (rand() % n); 89 | Nat result = minInclusive + r; 90 | return result; 91 | } 92 | 93 | 94 | static Real getRandomReal(Real minInclusive, Real maxExclusive) { 95 | 96 | Real r = rand()/(Real) RAND_MAX; 97 | Real result = minInclusive + r*(maxExclusive - minInclusive); 98 | return result; 99 | } 100 | 101 | 102 | static RealArray getRandomRealArray(Real minInclusive, Real maxExclusive, Nat numElems) { 103 | assert( maxExclusive > minInclusive ); 104 | assert( numElems > 0 ); 105 | 106 | RealArray arr = RealArray(numElems); 107 | for (Real& elem : arr) 108 | elem = getRandomReal(minInclusive, maxExclusive); 109 | 110 | return arr; 111 | } 112 | 113 | 114 | static Amp getRandomAmp() { 115 | 116 | // generate 2 normally-distributed random numbers via Box-Muller (MPI-safe) 117 | Real a = rand()/(Real) RAND_MAX; 118 | Real b = rand()/(Real) RAND_MAX; 119 | Real r1 = sqrt(-2 * log(a)) * cos(2 * 3.14159265 * b); 120 | Real r2 = sqrt(-2 * log(a)) * sin(2 * 3.14159265 * b); 121 | return Amp(r1, r2); 122 | } 123 | 124 | 125 | static AmpArray getRandomArray(Index dim) { 126 | assert( dim > 0 ); 127 | 128 | AmpArray arr = AmpArray(dim); 129 | for (Index i=0; i 0 ); 137 | 138 | AmpMatrix matr = getZeroMatrix(dim); 139 | for (Index i=0; i 0 ); 148 | assert( numMatrices > 0 ); 149 | 150 | MatrixArray matrices(numMatrices); 151 | for (AmpMatrix& matrix : matrices) 152 | matrix = getRandomMatrix(dim); 153 | 154 | return matrices; 155 | } 156 | 157 | 158 | static NatArray getRandomNatArray(Nat minIncl, Nat maxExcl, Nat numElem) { 159 | assert( maxExcl > minIncl ); 160 | assert( numElem > 0 ); 161 | 162 | NatArray choices(numElem); 163 | for (Nat i=0; i minIncl ); 172 | assert( numElem > 0 ); 173 | Nat numAllChoices = maxExcl - minIncl; 174 | assert( numElem <= numAllChoices ); 175 | 176 | // generate [minIncl,maxExcl) 177 | NatArray allChoices(numAllChoices); 178 | for (Nat i=0; i 0) { 184 | Nat i = getRandomNat(0, numAllChoices); 185 | Nat j = getRandomNat(0, numAllChoices); 186 | std::swap(allChoices[i], allChoices[j]); 187 | } 188 | 189 | // select first numElem 190 | NatArray choices(allChoices.begin(), allChoices.begin()+numElem); 191 | return choices; 192 | } 193 | 194 | 195 | static NatArray getRandomUniqueNatArray(Nat minIncl, Nat maxExcl, Nat numElem, Nat exclude) { 196 | assert( exclude >= minIncl && exclude < maxExcl ); 197 | assert( (maxExcl - minIncl) > numElem ); // we need 1 for exclude 198 | 199 | NatArray choices = getRandomUniqueNatArray(minIncl, maxExcl, numElem); 200 | 201 | for (Nat i=0; i qb2) 248 | std::swap(qb1, qb2); 249 | 250 | if (qb1 == qb2) 251 | return getIdentityMatrix(powerOf2(numQb)); 252 | 253 | AmpMatrix swap; 254 | 255 | if (qb2 == qb1 + 1) { 256 | // qubits are adjacent 257 | swap = AmpMatrix{{1,0,0,0},{0,0,1,0},{0,1,0,0},{0,0,0,1}}; 258 | 259 | } else { 260 | // qubits are distant 261 | Index block = powerOf2(qb2 - qb1); 262 | swap = getZeroMatrix(block*2); 263 | AmpMatrix iden = getIdentityMatrix(block/2); 264 | 265 | // Lemma 3.1 of arxiv.org/pdf/1711.09765.pdf 266 | AmpMatrix p0{{1,0},{0,0}}; 267 | AmpMatrix l0{{0,1},{0,0}}; 268 | AmpMatrix l1{{0,0},{1,0}}; 269 | AmpMatrix p1{{0,0},{0,1}}; 270 | 271 | /* notating a^(n+1) = identity(1< 0) 283 | swap = swap % getIdentityMatrix(powerOf2(qb1)); 284 | if (qb2 < numQb-1) 285 | swap = getIdentityMatrix(powerOf2(numQb-qb2-1)) % swap; 286 | 287 | return swap; 288 | } 289 | 290 | 291 | static bool containsDuplicate(NatArray list1, NatArray list2) { 292 | NatArray combined = list1; 293 | combined.insert(combined.end(), list2.begin(), list2.end()); 294 | std::sort(combined.begin(), combined.end()); 295 | for (Nat i=0; i 0 ); 304 | assert( ctrls.size() + targs.size() <= numQb ); 305 | assert( gateMatr.size() == powerOf2(targs.size()) ); 306 | assert( !containsDuplicate(ctrls, targs) ); 307 | assert( *std::max_element(targs.begin(), targs.end()) < numQb ); 308 | if (ctrls.size() > 0) 309 | assert( *std::max_element(ctrls.begin(), ctrls.end()) < numQb ); 310 | 311 | // concatenate all qubits 312 | NatArray qubits = targs; 313 | qubits.insert( qubits.end(), ctrls.begin(), ctrls.end() ); 314 | 315 | // create sub-matrix with qubits #(ctrls+targs), with left-most controlled and 316 | // rightmost targetted as if qubits = {0,1,2,3,...} 317 | Index subDim = powerOf2(qubits.size()); 318 | Index subInd = subDim - gateMatr.size(); 319 | AmpMatrix subMatr = getIdentityMatrix(subDim); 320 | setSubMatrix(subMatr, gateMatr, subInd, subInd); 321 | 322 | // pad sub-matrix to #numQb 323 | AmpMatrix fullMatr; 324 | if (numQb == qubits.size()) 325 | fullMatr = subMatr; 326 | else { 327 | Index idenDim = powerOf2(numQb - qubits.size()); 328 | fullMatr = getIdentityMatrix(idenDim) % subMatr; 329 | } 330 | 331 | // create swap matrices which sort qubits 332 | Index fullDim = powerOf2(numQb); 333 | AmpMatrix swapsMatr = getIdentityMatrix(fullDim); 334 | AmpMatrix unswapsMatr = getIdentityMatrix(fullDim); 335 | 336 | for (Nat q=0; qnumNodes == 1) 424 | return this->amps; 425 | 426 | Index numAllAmps = this->numNodes * this->numAmpsPerNode; 427 | AmpArray allAmps(numAllAmps); 428 | 429 | MPI_Allgather( 430 | (this->amps).data(), this->numAmpsPerNode, MPI_AMP, 431 | allAmps.data(), this->numAmpsPerNode, MPI_AMP, 432 | MPI_COMM_WORLD); 433 | 434 | return allAmps; 435 | } 436 | 437 | 438 | void StateVector::setRandomAmps() { 439 | 440 | comm_synch(); 441 | 442 | // all nodes generate all amps to keep RNG in-synch 443 | for (Nat rank=0; ranknumNodes; rank++) { 444 | AmpArray amps = getRandomArray(this->numAmpsPerNode); 445 | if (this->rank == rank) 446 | this->amps = amps; 447 | } 448 | } 449 | 450 | 451 | void StateVector::printAmps() { 452 | 453 | comm_synch(); 454 | 455 | // nominated rank prints while others wait 456 | for (Nat rank=0; ranknumNodes; rank++) { 457 | if (this->rank == rank) { 458 | printf("rank %u:\n", rank); 459 | printArray(this->amps); 460 | } 461 | 462 | comm_synch(); 463 | } 464 | 465 | std::cout << std::flush; 466 | comm_synch(); 467 | } 468 | 469 | 470 | bool StateVector::agreesWith(AmpArray ref, Real tol=1E-5) { 471 | assert( ref.size() == powerOf2(this->numQubits) ); 472 | 473 | comm_synch(); 474 | 475 | AmpArray allAmps = this->getAllVecAmps(); 476 | 477 | for (Index i=0; i tol || imag(dif) > tol) { 480 | if (this->rank == 0) 481 | printf("disagreement of (%g) + i(%g)\n", real(dif), imag(dif)); 482 | return false; 483 | } 484 | } 485 | 486 | return true; 487 | } 488 | 489 | 490 | AmpMatrix DensityMatrix::getAllMatrAmps() { 491 | 492 | comm_synch(); 493 | 494 | Index dim = powerOf2(this->numQubits); 495 | AmpArray vec = this->getAllVecAmps(); 496 | AmpMatrix matr = getZeroMatrix(dim); 497 | 498 | for (Index r=0; rnumQubits) ); 508 | 509 | comm_synch(); 510 | 511 | AmpMatrix allAmps = this->getAllMatrAmps(); 512 | 513 | for (Index r=0; r tol || imag(dif) > tol) { 517 | if (this->rank == 0) 518 | printf("disagreement of (%g) + i(%g)\n", real(dif), imag(dif)); 519 | return false; 520 | } 521 | } 522 | 523 | return true; 524 | } 525 | 526 | 527 | #endif // TEST_UTILITIES_HPP 528 | -------------------------------------------------------------------------------- /tests/tests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | #define CATCH_AMALGAMATED_CUSTOM_MAIN 3 | #include "catch_amalgamated.hpp" 4 | 5 | #include "test_utilities.hpp" 6 | #include "tests_statevector.hpp" 7 | #include "tests_densitymatrix.hpp" 8 | 9 | #include "communication.hpp" 10 | 11 | 12 | int main( int argc, char* argv[] ) { 13 | comm_init(); 14 | int result = Catch::Session().run( argc, argv ); 15 | comm_end(); 16 | return result; 17 | } -------------------------------------------------------------------------------- /tests/tests_densitymatrix.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TESTS_DENSITYMATRIX_HPP 2 | #define TESTS_DENSITYMATRIX_HPP 3 | 4 | 5 | #include "types.hpp" 6 | #include "states.hpp" 7 | #include "distributed_densitymatrix.hpp" 8 | 9 | #include "test_utilities.hpp" 10 | #include "catch_amalgamated.hpp" 11 | 12 | #include 13 | 14 | 15 | int NUM_QUBITS_RHO = 5; 16 | int NUM_TRIALS_PER_RHO_TEST = 5000; 17 | 18 | 19 | #define PREPARE_RHO_TEST(rhoVar, refVar) \ 20 | GENERATE( range(0,NUM_TRIALS_PER_RHO_TEST) ); \ 21 | DensityMatrix rhoVar = DensityMatrix(NUM_QUBITS_RHO); \ 22 | rhoVar.setRandomAmps(); \ 23 | AmpMatrix refVar = rhoVar.getAllMatrAmps(); 24 | 25 | 26 | TEST_CASE( "densitymatrix_manyTargGate" ) { 27 | 28 | PREPARE_RHO_TEST( rho, ref ); 29 | 30 | Nat maxNumTargs = NUM_QUBITS_RHO - ceil(rho.logNumNodes / 2.); 31 | Nat numTargs = getRandomNat(1, maxNumTargs + 1); 32 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 33 | AmpMatrix gate = getRandomMatrix( powerOf2(numTargs) ); 34 | 35 | distributed_densitymatrix_manyTargGate(rho, targets, gate); 36 | applyGateToLocalState(ref, {}, targets, gate); 37 | 38 | REQUIRE( rho.agreesWith(ref) ); 39 | } 40 | 41 | 42 | TEST_CASE( "densitymatrix_swapGate" ) { 43 | 44 | PREPARE_RHO_TEST( rho, ref ); 45 | 46 | AmpMatrix gate = {{1,0,0,0}, {0,0,1,0}, {0,1,0,0}, {0,0,0,1}}; 47 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, 2); 48 | 49 | distributed_densitymatrix_swapGate(rho, targets[0], targets[1]); 50 | applyGateToLocalState(ref, {}, targets, gate); 51 | 52 | REQUIRE( rho.agreesWith(ref) ); 53 | } 54 | 55 | 56 | TEST_CASE( "densitymatrix_pauliTensor" ) { 57 | 58 | PREPARE_RHO_TEST( rho, ref ); 59 | 60 | Nat numTargs = getRandomNat(1, NUM_QUBITS_RHO + 1); 61 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 62 | NatArray paulis = getRandomNatArray(1, 4, numTargs); 63 | ensureNotAllPauliZ(paulis); 64 | AmpMatrix gate = getKroneckerProductOfPaulis(paulis); 65 | 66 | distributed_densitymatrix_pauliTensor(rho, targets, paulis); 67 | applyGateToLocalState(ref, {}, targets, gate); 68 | 69 | REQUIRE( rho.agreesWith(ref) ); 70 | } 71 | 72 | 73 | TEST_CASE( "densitymatrix_pauliGadget" ) { 74 | 75 | PREPARE_RHO_TEST( rho, ref ); 76 | 77 | Nat numTargs = getRandomNat(1, NUM_QUBITS_RHO + 1); 78 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 79 | NatArray paulis = getRandomNatArray(1, 4, numTargs); 80 | ensureNotAllPauliZ(paulis); 81 | Real theta = getRandomReal(-PI, PI); 82 | AmpMatrix tensor = getKroneckerProductOfPaulis(paulis); 83 | AmpMatrix gate = getExponentialOfPauliTensor(theta, tensor); 84 | 85 | distributed_densitymatrix_pauliGadget(rho, targets, paulis, theta); 86 | applyGateToLocalState(ref, {}, targets, gate); 87 | 88 | REQUIRE( rho.agreesWith(ref) ); 89 | } 90 | 91 | 92 | TEST_CASE( "densitymatrix_phaseGadget") { 93 | 94 | PREPARE_RHO_TEST( rho, ref ); 95 | 96 | Nat numTargs = getRandomNat(1, NUM_QUBITS_RHO + 1); 97 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 98 | Real theta = getRandomReal(-PI, PI); 99 | AmpMatrix tensor = getKroneckerProductOfPaulis(NatArray(numTargs, 3)); 100 | AmpMatrix gate = getExponentialOfPauliTensor(theta, tensor); 101 | 102 | distributed_densitymatrix_phaseGadget(rho, targets, theta); 103 | applyGateToLocalState(ref, {}, targets, gate); 104 | 105 | REQUIRE( rho.agreesWith(ref) ); 106 | } 107 | 108 | 109 | TEST_CASE( "densitymatrix_krausMap" ) { 110 | 111 | PREPARE_RHO_TEST( rho, ref ); 112 | 113 | Nat maxNumTargs = NUM_QUBITS_RHO - ceil(rho.logNumNodes / 2.); 114 | Nat numTargs = getRandomNat(1, maxNumTargs + 1); 115 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 116 | Nat numKrausOps = getRandomNat(1, 10); 117 | MatrixArray krausOps = getRandomMatrices(powerOf2(numTargs), numKrausOps); 118 | 119 | distributed_densitymatrix_krausMap(rho, krausOps, targets); 120 | applyKrausMapToLocalState(ref, targets, krausOps); 121 | 122 | REQUIRE( rho.agreesWith(ref) ); 123 | } 124 | 125 | 126 | TEST_CASE( "densitymatrix_oneQubitDephasing" ) { 127 | 128 | PREPARE_RHO_TEST( rho, ref ); 129 | 130 | Nat target = getRandomNat(0, NUM_QUBITS_RHO); 131 | Real prob = getRandomReal(0, 1/2.); 132 | MatrixArray krausOps(2); 133 | krausOps[0] = sqrt(1-prob) * matrI; 134 | krausOps[1] = sqrt(prob) * matrZ; 135 | 136 | distributed_densitymatrix_oneQubitDephasing(rho, target, prob); 137 | applyKrausMapToLocalState(ref, {target}, krausOps); 138 | 139 | REQUIRE( rho.agreesWith(ref) ); 140 | } 141 | 142 | 143 | TEST_CASE( "densitymatrix_twoQubitDephasing" ) { 144 | 145 | PREPARE_RHO_TEST( rho, ref ); 146 | 147 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, 2); 148 | Real prob = getRandomReal(0, 3/4.); 149 | MatrixArray krausOps(4); 150 | krausOps[0] = sqrt(1-prob) * getKroneckerProduct(matrI, matrI); 151 | krausOps[1] = sqrt(prob/3.) * getKroneckerProduct(matrI, matrZ); 152 | krausOps[2] = sqrt(prob/3.) * getKroneckerProduct(matrZ, matrI); 153 | krausOps[3] = sqrt(prob/3.) * getKroneckerProduct(matrZ, matrZ); 154 | 155 | distributed_densitymatrix_twoQubitDephasing(rho, targets[0], targets[1], prob); 156 | applyKrausMapToLocalState(ref, targets, krausOps); 157 | 158 | REQUIRE( rho.agreesWith(ref) ); 159 | } 160 | 161 | 162 | TEST_CASE( "densitymatrix_oneQubitDepolarising" ) { 163 | 164 | PREPARE_RHO_TEST( rho, ref ); 165 | 166 | Nat target = getRandomNat(0, NUM_QUBITS_RHO); 167 | Real prob = getRandomReal(0, 1/2.); 168 | MatrixArray krausOps(4); 169 | krausOps[0] = sqrt(1-prob) * matrI; 170 | krausOps[1] = sqrt(prob/3.) * matrX; 171 | krausOps[2] = sqrt(prob/3.) * matrY; 172 | krausOps[3] = sqrt(prob/3.) * matrZ; 173 | 174 | distributed_densitymatrix_oneQubitDepolarising(rho, target, prob); 175 | applyKrausMapToLocalState(ref, {target}, krausOps); 176 | 177 | REQUIRE( rho.agreesWith(ref) ); 178 | } 179 | 180 | 181 | TEST_CASE( "densitymatrix_twoQubitDepolarising" ) { 182 | 183 | PREPARE_RHO_TEST( rho, ref ); 184 | 185 | NatArray targets = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, 2); 186 | Real prob = getRandomReal(0, 3/4.); 187 | MatrixArray krausOps(16); 188 | Nat i=0; 189 | for (AmpMatrix matr1 : {matrI, matrX, matrY, matrZ}) 190 | for (AmpMatrix matr2 : {matrI, matrX, matrY, matrZ}) 191 | krausOps[i++] = sqrt(prob/15.) * getKroneckerProduct(matr1, matr2); 192 | krausOps[0] = sqrt(1-16/15.) * getIdentityMatrix( powerOf2(2) ); 193 | 194 | distributed_densitymatrix_twoQubitDepolarising(rho, targets[0], targets[1], prob); 195 | applyKrausMapToLocalState(ref, targets, krausOps); 196 | 197 | REQUIRE( rho.agreesWith(ref) ); 198 | } 199 | 200 | 201 | TEST_CASE( "densitymatrix_damping" ) { 202 | 203 | PREPARE_RHO_TEST( rho, ref ); 204 | 205 | Nat target = getRandomNat(0, NUM_QUBITS_RHO); 206 | Real prob = getRandomReal(0, 1/2.); 207 | MatrixArray krausOps(2); 208 | krausOps[0] = {{1,0},{0,sqrt(1-prob)}}; 209 | krausOps[1] = {{0,sqrt(prob)},{0,0}}; 210 | 211 | distributed_densitymatrix_damping(rho, target, prob); 212 | applyKrausMapToLocalState(ref, {target}, krausOps); 213 | 214 | REQUIRE( rho.agreesWith(ref) ); 215 | } 216 | 217 | 218 | TEST_CASE( "densitymatrix_expecPauliString" ) { 219 | 220 | PREPARE_RHO_TEST( rho, ref ); 221 | 222 | Nat numQubits = NUM_QUBITS_RHO; 223 | Nat numTerms = getRandomNat(1, 30); 224 | Nat numPaulis = numTerms * numQubits; 225 | NatArray paulis = getRandomNatArray(0, 4, numPaulis); 226 | RealArray coeffs = getRandomRealArray(-10, 10, numTerms); 227 | 228 | Amp expec1 = distributed_densitymatrix_expecPauliString(rho, coeffs, paulis); 229 | 230 | AmpMatrix pauliStringMatr = getZeroMatrix( powerOf2(numQubits) ); 231 | for (Nat i=0; i=1 columns per-node. 252 | // this is _not_ essential for the algorithm, which actually permits looser 253 | // maxNumTargs = NUM_QUBITS_RHO - (rho.logNumNodes/2), 254 | // but required as a precondition to our density-matrix constructor, to 255 | // maintain compatibility with the other algorithms in this project 256 | Nat maxNumTargs = NUM_QUBITS_RHO - rho.logNumNodes; 257 | 258 | Nat numTargs = getRandomNat(1, maxNumTargs + 1); 259 | NatArray targs = getRandomUniqueNatArray(0, NUM_QUBITS_RHO, numTargs); 260 | 261 | DensityMatrix rhoOut = distributed_densitymatrix_partialTrace(rho, targs); 262 | 263 | // serially populate the reference reduced matrix 264 | AmpMatrix refOut = getZeroMatrix( powerOf2(NUM_QUBITS_RHO - numTargs) ); 265 | 266 | std::sort(targs.begin(), targs.end()); 267 | for (Index i=0; i