Maestro 0.2.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
MPSDummySimulator.h
Go to the documentation of this file.
1
15
16#pragma once
17
18#ifndef _MPSDUMMYSIMULATOR_H_
19#define _MPSDUMMYSIMULATOR_H_
20
21#include <algorithm>
22#include <deque>
23#include <random>
24
25#include <Eigen/Eigen>
26#include <unsupported/Eigen/CXX11/Tensor>
27
28#include "QuantumGate.h"
29#include "MPSSimulatorInterface.h"
30
31namespace Simulators {
32
34 public:
35 using IndexType = long long int;
36 using MatrixClass = QC::TensorNetworks::MPSSimulatorInterface::MatrixClass;
37 using GateClass = QC::TensorNetworks::MPSSimulatorInterface::GateClass;
38
39 MPSDummySimulator(size_t N) : nrQubits(N), maxVirtualExtent(0) {
40 InitQubitsMap();
41
43 }
44
45 std::unique_ptr<MPSDummySimulator> Clone() const {
46 auto clone = std::unique_ptr<MPSDummySimulator>(
47 new MPSDummySimulator(nrQubits, LightweightInitTag{}));
48 clone->qubitsMap = qubitsMap;
49 clone->qubitsMapInv = qubitsMapInv;
50 clone->maxVirtualExtent = maxVirtualExtent;
51 clone->bondCost = bondCost;
52 clone->maxBondDim = maxBondDim;
53 clone->currentBondDim = currentBondDim;
54
55 clone->totalSwappingCost = totalSwappingCost;
56
57 clone->growthFactorSwap = growthFactorSwap;
58 clone->growthFactorGate = growthFactorGate;
59
60 return clone;
61 }
62
63 size_t getNrQubits() const { return nrQubits; }
64
65 double getGrowthFactorSwap() const { return growthFactorSwap; }
66 double getGrowthFactorGate() const { return growthFactorGate; }
67
68 void setGrowthFactorSwap(double factor) { growthFactorSwap = factor; }
69 void setGrowthFactorGate(double factor) { growthFactorGate = factor; }
70
71 void Clear() { InitQubitsMap(); }
72
74 maxVirtualExtent = val;
75
76 if (nrQubits == 0) return;
77
78 const double untruncatedMaxExtent = std::pow(physExtent, nrQubits / 2);
79 IndexType maxVirtualExtentLimit =
80 static_cast<IndexType>(untruncatedMaxExtent);
81
82 if (untruncatedMaxExtent >= std::numeric_limits<IndexType>::max() ||
83 std::isnan(untruncatedMaxExtent) || std::isinf(untruncatedMaxExtent))
84 maxVirtualExtentLimit = std::numeric_limits<IndexType>::max() - 1;
85 else if (untruncatedMaxExtent < 2)
86 maxVirtualExtentLimit = 2;
87
88 maxVirtualExtent = maxVirtualExtent == 0
89 ? maxVirtualExtentLimit
90 : std::min(maxVirtualExtent, maxVirtualExtentLimit);
91
92 bondCost.resize(nrQubits - 1);
93 maxBondDim.resize(nrQubits - 1);
94 currentBondDim.assign(nrQubits - 1, 1.0);
95
96 // the checks here are overkill, but better safe than sorry
97 // we're dealing with large values here, overflows are to be expected
98 for (IndexType i = 0; i < static_cast<IndexType>(nrQubits); ++i) {
99 double maxExtent1 = std::pow((double)physExtent, (double)i + 1.);
100 double maxExtent2 =
101 std::pow((double)physExtent, (double)nrQubits - i - 1.);
102
103 if (maxExtent1 >= (double)std::numeric_limits<size_t>::max() ||
104 std::isnan(maxExtent1) || std::isinf(maxExtent1))
105 maxExtent1 = (double)std::numeric_limits<size_t>::max() - 1;
106 else if (maxExtent1 < 1)
107 maxExtent1 = 1;
108
109 if (maxExtent2 > (double)std::numeric_limits<size_t>::max() ||
110 std::isnan(maxExtent2) || std::isinf(maxExtent2))
111 maxExtent2 = (double)std::numeric_limits<size_t>::max() - 1;
112 else if (maxExtent2 < 1)
113 maxExtent2 = 1;
114
115 size_t maxRightExtent = (size_t)std::min<double>(
116 {maxExtent1, maxExtent2, (double)maxVirtualExtent});
117 if (maxRightExtent < 2) maxRightExtent = 2;
118
119 if (i < static_cast<IndexType>(nrQubits) - 1) {
120 maxBondDim[i] = static_cast<double>(maxRightExtent);
121 const double maxRightExtentD = static_cast<double>(maxRightExtent);
122 bondCost[i] = maxRightExtentD * maxRightExtentD * maxRightExtentD;
123 }
124 }
125 }
126
127 void print() const {
128 std::cout << "Qubits map: ";
129 for (int q = 0; q < static_cast<int>(qubitsMap.size()); ++q)
130 std::cout << q << "->" << qubitsMap[q] << " ";
131 std::cout << std::endl;
132 }
133
134 void ApplyGate(const QC::Gates::AppliedGate<MatrixClass>& gate) {
135 ApplyGate(gate, gate.getQubit1(), gate.getQubit2());
136 }
137
138 void ApplyGate(const std::shared_ptr<Circuits::IOperation<>>& gate) {
139 const auto qbits = gate->AffectedQubits();
140
141 if (qbits.size() == 1) {
142 // 1-qubit gates are no-ops in the dummy simulator (no swap logic)
143 return;
144 } else if (qbits.size() == 2) {
145 // Reuse a single static dummy 2-qubit gate to avoid heap allocation.
146 // Only getQubitsNumber() is checked; the matrix is never read.
147 static const QC::Gates::AppliedGate<MatrixClass> dummy2qGate(
148 MatrixClass::Identity(4, 4), 0, 1);
149 ApplyGate(dummy2qGate, qbits[0], qbits[1]);
150 } else if (qbits.size() == 3 && gate->GetType() == Circuits::OperationType::kGate) {
151 static const QC::Gates::AppliedGate<MatrixClass> dummy2qGate(
152 MatrixClass::Identity(4, 4), 0, 1);
153
154 const auto gateptr =
155 std::static_pointer_cast<Circuits::IQuantumGate<>>(gate);
156
157 const size_t q1 = qbits[0]; // control 1
158 const size_t q2 = qbits[1]; // control 2
159 const size_t q3 = qbits[2]; // target
160 if (gateptr->GetGateType() == Circuits::QuantumGateType::kCCXGateType) {
161 ApplyGate(dummy2qGate, q3, q2);
162 ApplyGate(dummy2qGate, q2, q1);
163 ApplyGate(dummy2qGate, q3, q2);
164 ApplyGate(dummy2qGate, q2, q1);
165 ApplyGate(dummy2qGate, q3, q1);
166 } else { // cswap
167 ApplyGate(dummy2qGate, q2, q3);
168 ApplyGate(dummy2qGate, q3, q2);
169 ApplyGate(dummy2qGate, q2, q1);
170 ApplyGate(dummy2qGate, q3, q2);
171 ApplyGate(dummy2qGate, q2, q1);
172 ApplyGate(dummy2qGate, q3, q1);
173 ApplyGate(dummy2qGate, q2, q3);
174 }
175 }
176 }
177
178 void ApplyGate(const GateClass& gate, IndexType qubit,
179 IndexType controllingQubit1 = 0) {
180 if (qubit < 0 || qubit >= static_cast<IndexType>(getNrQubits()))
181 throw std::invalid_argument("Qubit index out of bounds");
182 else if (controllingQubit1 < 0 ||
183 controllingQubit1 >= static_cast<IndexType>(getNrQubits()))
184 throw std::invalid_argument("Qubit index out of bounds");
185
186 // for two qubit gates:
187 // if the qubits are not adjacent, apply swap gates until they are
188 // don't forget to update the qubitsMap
189 if (gate.getQubitsNumber() > 1) {
190 IndexType qubit1 = qubitsMap[qubit];
191 IndexType qubit2 = qubitsMap[controllingQubit1];
192
193 if (abs(qubit1 - qubit2) > 1) {
194 SwapQubits(qubit, controllingQubit1);
195 qubit1 = qubitsMap[qubit];
196 qubit2 = qubitsMap[controllingQubit1];
197 assert(abs(qubit1 - qubit2) == 1);
198 }
199
200 // any 2-qubit gate (whether swaps were needed or not) grows the
201 // bond dimension at the bond between the two adjacent qubits.
202 // Charge the cost based on the bond dimension *before* the growth,
203 // consistent with SwapQubitsToPosition: the cost reflects the size of
204 // the SVD/contraction actually performed for this operation, and the
205 // grown bond dimension only affects subsequent operations.
206 const IndexType bond = std::min(qubit1, qubit2);
207 totalSwappingCost += bondCost[bond];
208 // TODO: This is basic, the two qubit gates can have rank 2 and 4 (1 if
209 // can be decomposed into two 1-qubit gates)
210 // the controlled ones have Schmidt rank 2
211 // here we assume that the others have Schmidt rank 4,
212 // but that might not be the case for all gates
213 // (3 is not possible for two qubit gates)
214 growBondDimension(bond, false, gate.isControlled() ? 2 : 4);
215 }
216 }
217
219 const std::vector<QC::Gates::AppliedGate<MatrixClass>>& gates) {
220 for (const auto& gate : gates) ApplyGate(gate);
221 }
222
224 const std::vector<std::shared_ptr<Circuits::IOperation<>>>& gates) {
225 for (const auto& gate : gates) ApplyGate(gate);
226 }
227
228 void SetInitialQubitsMap(const std::vector<long long int>& initialMap) {
229 qubitsMap = initialMap;
230 for (size_t i = 0; i < initialMap.size(); ++i)
231 qubitsMapInv[initialMap[i]] = i;
232
233 totalSwappingCost = 0;
234 std::fill(currentBondDim.begin(), currentBondDim.end(), 1.0);
235 for (size_t i = 0; i < bondCost.size(); ++i) bondCost[i] = 1;
236 }
237
238 void SetCurrentBondDimensions(const std::vector<double>& dims) {
239 assert(dims.size() == currentBondDim.size());
240 for (size_t i = 0; i < dims.size(); ++i)
241 currentBondDim[i] = std::min(dims[i], maxBondDim[i]);
242
243 // update bondCost as well, since it depends on the current bond dimensions
244 for (size_t i = 0; i < currentBondDim.size(); ++i)
245 bondCost[i] = currentBondDim[i] * currentBondDim[i] * currentBondDim[i];
246 }
247
248 const std::vector<double>& getCurrentBondDimensions() const {
249 return currentBondDim;
250 }
251
252 const std::vector<double>& getMaxBondDimensions() const { return maxBondDim; }
253
254 const std::vector<IndexType>& getQubitsMap() const { return qubitsMap; }
255
256 const std::vector<IndexType>& getQubitsMapInv() const { return qubitsMapInv; }
257
258 void setTotalSwappingCost(double cost) { totalSwappingCost = cost; }
259 double getTotalSwappingCost() const { return totalSwappingCost; }
260
261 // Evaluate the total cost
262 // position, applying the current 2-qubit gate, and then simulating the
263 // next lookaheadDepth 2-qubit gates from upcomingGates.
265 IndexType meetPosition,
266 const std::vector<std::shared_ptr<Circuits::IOperation<>>>& upcomingGates,
267 long long int currentGateIndex, int lookaheadDepth,
268 int lookaheadDepthWithHeuristic, double currentCost, double& bestCost,
269 bool useSameDummy = false) {
270 if (currentGateIndex >= static_cast<long long int>(upcomingGates.size())) {
271 if (currentCost < bestCost) bestCost = currentCost;
272 return;
273 }
274
275 // skip the 1 qubit gates, advance to the next 2-qubit gate
276 while (currentGateIndex <
277 static_cast<long long int>(upcomingGates.size()) &&
278 upcomingGates[currentGateIndex]->AffectedQubits().size() < 2)
279 ++currentGateIndex;
280
281 if (currentGateIndex >= static_cast<long long int>(upcomingGates.size())) {
282 if (currentCost < bestCost) bestCost = currentCost;
283 return;
284 }
285
286 const auto& op = upcomingGates[currentGateIndex];
287 const auto qbits = op->AffectedQubits();
288
289 assert(qbits.size() >= 2);
290
291 const IndexType qubit1 = static_cast<IndexType>(qbits[0]);
292 const IndexType qubit2 = static_cast<IndexType>(qbits[1]);
293
294 // Perform the swap to the meeting position;
295 // totalSwappingCost starts at 0 and SwapQubitsToPosition accumulates
296 // per-bond costs, so the cost correctly depends on meetPosition.
297
298 IndexType realq1 = qubitsMap[qubit1];
299 IndexType realq2 = qubitsMap[qubit2];
300 if (realq1 > realq2) std::swap(realq1, realq2);
301
302 if (useSameDummy) {
303 const double ccost = getTotalSwappingCost();
304
305 if (realq2 - realq1 > 1)
306 SwapQubitsToPosition(qubit1, qubit2, meetPosition);
307
308 ApplyGate(op);
309
310 // const double importanceFactor =
311 // pow(dimFactor, (lookaheadDepthWithHeuristic - lookaheadDepth - 1));
312 currentCost += (getTotalSwappingCost() - ccost) ;
313
314 if (currentCost >= bestCost) return;
315 // No more lookahead depth: return without further recursion
316 if (lookaheadDepth <= 0) {
317 if (currentCost < bestCost) bestCost = currentCost;
318 return;
319 }
320
321 // skip the 1 qubit gates, advance over the current gate on the next
322 // 2-qubit gate
323 ++currentGateIndex;
324 while (currentGateIndex <
325 static_cast<long long int>(upcomingGates.size()) &&
326 upcomingGates[currentGateIndex]->AffectedQubits().size() < 2)
327 ++currentGateIndex;
328
329 if (currentGateIndex >=
330 static_cast<long long int>(upcomingGates.size())) {
331 if (currentCost < bestCost) bestCost = currentCost;
332 return;
333 }
334
335 FindBestMeetingPosition(upcomingGates, currentGateIndex,
336 lookaheadDepth - 1, lookaheadDepthWithHeuristic,
337 currentCost, bestCost);
338 } else {
339 MPSDummySimulator dummySim(nrQubits, LightweightInitTag{});
340 dummySim.maxVirtualExtent = maxVirtualExtent;
341 dummySim.maxBondDim = maxBondDim;
342 dummySim.currentBondDim = currentBondDim;
343 dummySim.bondCost = bondCost;
344 dummySim.qubitsMap = qubitsMap;
345 dummySim.qubitsMapInv.resize(qubitsMap.size());
346 for (size_t i = 0; i < qubitsMap.size(); ++i)
347 dummySim.qubitsMapInv[qubitsMap[i]] = static_cast<IndexType>(i);
348
349 dummySim.setTotalSwappingCost(0);
350
351 if (realq2 - realq1 > 1)
352 dummySim.SwapQubitsToPosition(qubit1, qubit2, meetPosition);
353
354 dummySim.ApplyGate(op); // this also updates the bond dimensions in
355 // dummySim according to the applied gate
356
357 currentCost += dummySim.getTotalSwappingCost();
358
359 // Pruning: if current accumulated cost already exceeds best known, prune
360 if (currentCost >= bestCost) return;
361
362 // No more lookahead depth: return without further recursion
363 if (lookaheadDepth <= 0) {
364 if (currentCost < bestCost) bestCost = currentCost;
365 return;
366 }
367
368 // skip the 1 qubit gates, advance over the current gate on the next
369 // 2-qubit gate
370 ++currentGateIndex;
371 while (currentGateIndex <
372 static_cast<long long int>(upcomingGates.size()) &&
373 upcomingGates[currentGateIndex]->AffectedQubits().size() < 2)
374 ++currentGateIndex;
375
376 if (currentGateIndex >=
377 static_cast<long long int>(upcomingGates.size())) {
378 if (currentCost < bestCost) bestCost = currentCost;
379 return;
380 }
381
382 dummySim.FindBestMeetingPosition(
383 upcomingGates, currentGateIndex, lookaheadDepth - 1,
384 lookaheadDepthWithHeuristic, currentCost, bestCost);
385 }
386 }
387
388 // Find the meeting position that minimizes the combined cost of the
389 // current swap + lookahead gates. Returns the optimal bond index.
390 // outBestCost receives the total accumulated cost for the best position.
392 const std::vector<std::shared_ptr<Circuits::IOperation<>>>& upcomingGates,
393 long long int currentGateIndex, int lookaheadDepth,
394 int lookaheadDepthWithHeuristic, double currentCost, double& bestCost) {
395 const auto& op = upcomingGates[currentGateIndex];
396 const auto qbits = op->AffectedQubits();
397
398 assert(qbits.size() >= 2);
399
400 const IndexType qubit1 = static_cast<IndexType>(qbits[0]);
401 const IndexType qubit2 = static_cast<IndexType>(qbits[1]);
402
403 IndexType realq1 = qubitsMap[qubit1];
404 IndexType realq2 = qubitsMap[qubit2];
405
406 if (realq1 > realq2) std::swap(realq1, realq2);
407
408 if (lookaheadDepth <= 0) {
409 IndexType pos = (realq2 - realq1 > 1)
410 ? ComputeHeuristicMeetPosition(realq1, realq2)
411 : realq1;
412 EvaluateMeetingPositionCost(pos, upcomingGates, currentGateIndex, 0,
413 lookaheadDepthWithHeuristic, currentCost,
414 bestCost, false);
415 return pos;
416 }
417
418 if (realq2 - realq1 <= 1) {
420 realq1, upcomingGates, currentGateIndex, lookaheadDepth,
421 lookaheadDepthWithHeuristic, currentCost, bestCost,
422 lookaheadDepth <= lookaheadDepthWithHeuristic);
423
424 return realq1;
425 }
426
427 IndexType bestPosition = realq1;
428
429 if (lookaheadDepth <= lookaheadDepthWithHeuristic) {
430 bestPosition = ComputeHeuristicMeetPosition(realq1, realq2);
431 EvaluateMeetingPositionCost(bestPosition, upcomingGates, currentGateIndex,
432 lookaheadDepth, lookaheadDepthWithHeuristic,
433 currentCost, bestCost, true);
434 } else {
435 for (IndexType m = realq1; m < realq2; ++m) {
436 const double oldCost = bestCost;
437 EvaluateMeetingPositionCost(m, upcomingGates, currentGateIndex,
438 lookaheadDepth, lookaheadDepthWithHeuristic,
439 currentCost, bestCost);
440
441 if (bestCost < oldCost) bestPosition = m;
442 }
443 }
444
445 return bestPosition;
446 }
447
448 std::vector<long long int> ComputeOptimalQubitsMap(
449 const std::vector<std::shared_ptr<Circuits::Circuit<>>>& layers,
450 int nrShuffles = 0/*25*/, int nrSwaps = 0/*10*/) {
451 const IndexType nrQubits = getNrQubits();
452
453 if (layers.empty() || nrQubits <= 2) return qubitsMap;
454
455
456 auto evaluateCost =
457 [&, this](const std::vector<IndexType>& candidateMap) -> double {
458 auto saveQubitsMap = qubitsMap;
459 auto saveQubitsMapInv = qubitsMapInv;
460 auto saveCurrentBondDim = currentBondDim;
461 auto saveTotalSwappingCost = totalSwappingCost;
462 auto saveBondCost = bondCost;
463
464 SetInitialQubitsMap(candidateMap);
465
466 for (const auto& layer : layers)
467 ApplyGates(layer->GetOperations());
468 auto cost = getTotalSwappingCost();
469 qubitsMap = std::move(saveQubitsMap);
470 qubitsMapInv = std::move(saveQubitsMapInv);
471 currentBondDim = std::move(saveCurrentBondDim);
472 totalSwappingCost = saveTotalSwappingCost;
473 bondCost = std::move(saveBondCost);
474
475 return cost;
476 };
477
478 auto evaluateCostBounded = [&, this](
479 const std::vector<IndexType>& candidateMap,
480 double bound) -> double {
481 auto saveQubitsMap = qubitsMap;
482 auto saveQubitsMapInv = qubitsMapInv;
483 auto saveCurrentBondDim = currentBondDim;
484 auto saveTotalSwappingCost = totalSwappingCost;
485 auto saveBondCost = bondCost;
486
487 SetInitialQubitsMap(candidateMap);
488
489 for (const auto& layer : layers) {
490 ApplyGates(layer->GetOperations());
491 auto cost = getTotalSwappingCost();
492 if (cost >= bound) {
493 // restore state
494 qubitsMap = std::move(saveQubitsMap);
495 qubitsMapInv = std::move(saveQubitsMapInv);
496 currentBondDim = std::move(saveCurrentBondDim);
497 totalSwappingCost = saveTotalSwappingCost;
498 bondCost = std::move(saveBondCost);
499 return cost;
500 }
501 }
502 auto cost = getTotalSwappingCost();
503 // restore state
504 qubitsMap = std::move(saveQubitsMap);
505 qubitsMapInv = std::move(saveQubitsMapInv);
506 currentBondDim = std::move(saveCurrentBondDim);
507 totalSwappingCost = saveTotalSwappingCost;
508 bondCost = std::move(saveBondCost);
509
510 return cost;
511 };
512
513
514 // Collect 2-qubit pairs from each layer, preserving layer boundaries
515 struct QubitPair {
516 IndexType q1, q2;
517 };
518 std::vector<std::vector<QubitPair>> layerPairs;
519
520 for (size_t li = 0; li < layers.size(); ++li) {
521 const auto& layer = layers[li];
522 std::vector<QubitPair> lp;
523 for (const auto& op : layer->GetOperations()) {
524 auto qbits = op->AffectedQubits();
525 if (qbits.size() >= 2) {
526 if (qbits[0] > qbits[1]) std::swap(qbits[0], qbits[1]);
527
528 lp.push_back({static_cast<IndexType>(qbits[0]),
529 static_cast<IndexType>(qbits[1])});
530 }
531 }
532 if (!lp.empty()) layerPairs.push_back(std::move(lp));
533 }
534 if (layerPairs.empty()) return qubitsMap;
535
536 // Build a linear qubit chain from ordered pairs using a group-merging
537 // strategy. Pairs are processed in order (earlier = more important).
538 // - If neither qubit is placed, create a new group [q1, q2].
539 // - If one qubit is already in a group, add the other at the group
540 // end (front or back) that minimizes distance to the existing one.
541 // - If both are in different groups, merge as [gA][gB] or [gB][gA],
542 // whichever places q1 and q2 closer together.
543 // - If both are already in the same group, do nothing.
544 auto buildChain = [&](const std::vector<std::vector<QubitPair>>& orderedLP)
545 -> std::vector<long long int> {
546 std::vector<std::deque<IndexType>> groups;
547 std::vector<int> qubitGroup(nrQubits, -1);
548 std::unordered_set<IndexType> placedQubits;
549
550 for (const auto& lp : orderedLP) {
551 for (const auto& p : lp) {
552 const int g1 = qubitGroup[p.q1];
553 const int g2 = qubitGroup[p.q2];
554
555 if (g1 < 0 && g2 < 0) {
556 // Neither placed: create a new group
557 const int gIdx = static_cast<int>(groups.size());
558 groups.push_back({p.q1, p.q2});
559 qubitGroup[p.q1] = gIdx;
560 qubitGroup[p.q2] = gIdx;
561 placedQubits.insert(p.q1);
562 placedQubits.insert(p.q2);
563 } else if (g1 >= 0 && g2 < 0) {
564 // q1 placed, q2 not: add q2 at the closer end of q1's group
565 auto& grp = groups[g1];
566 size_t idx = 0;
567 for (size_t i = 0; i < grp.size(); ++i)
568 if (grp[i] == p.q1) {
569 idx = i;
570 break;
571 }
572 // front distance: idx + 1, back distance: grp.size() - idx
573 if (idx + 1 <= grp.size() - idx)
574 grp.push_front(p.q2);
575 else
576 grp.push_back(p.q2);
577 qubitGroup[p.q2] = g1;
578 placedQubits.insert(p.q2);
579 } else if (g1 < 0 && g2 >= 0) {
580 // q2 placed, q1 not: add q1 at the closer end of q2's group
581 auto& grp = groups[g2];
582 size_t idx = 0;
583 for (size_t i = 0; i < grp.size(); ++i)
584 if (grp[i] == p.q2) {
585 idx = i;
586 break;
587 }
588 if (idx + 1 <= grp.size() - idx)
589 grp.push_front(p.q1);
590 else
591 grp.push_back(p.q1);
592 qubitGroup[p.q1] = g2;
593 placedQubits.insert(p.q1);
594 } else if (g1 != g2) {
595 // Both in different groups: merge to minimize distance
596 auto& grpA = groups[g1];
597 auto& grpB = groups[g2];
598 size_t idxA = 0, idxB = 0;
599 for (size_t i = 0; i < grpA.size(); ++i)
600 if (grpA[i] == p.q1) {
601 idxA = i;
602 break;
603 }
604 for (size_t i = 0; i < grpB.size(); ++i)
605 if (grpB[i] == p.q2) {
606 idxB = i;
607 break;
608 }
609 // [A][B]: q1 at idxA, q2 at |A|+idxB -> dist = |A|+idxB-idxA
610 // [B][A]: q2 at idxB, q1 at |B|+idxA -> dist = |B|+idxA-idxB
611 const size_t distAB = grpA.size() + idxB - idxA;
612 const size_t distBA = grpB.size() + idxA - idxB;
613
614 int mergedGroup;
615 if (distAB <= distBA) {
616 for (const auto q : grpB) grpA.push_back(q);
617 grpB.clear();
618 mergedGroup = g1;
619 } else {
620 for (const auto q : grpA) grpB.push_back(q);
621 grpA.clear();
622 mergedGroup = g2;
623 }
624 for (const auto q : groups[mergedGroup])
625 qubitGroup[q] = mergedGroup;
626 }
627 // Both in the same group: do nothing
628
629 if (placedQubits.size() == static_cast<size_t>(nrQubits))
630 break; // all qubits placed, can stop processing pairs
631 }
632
633 if (placedQubits.size() == static_cast<size_t>(nrQubits))
634 break; // all qubits placed, can stop processing pairs
635 }
636
637 // Concatenate all non-empty groups
638 std::vector<IndexType> chain;
639 chain.reserve(nrQubits);
640 for (const auto& grp : groups)
641 for (const auto q : grp) chain.push_back(q);
642
643 // Append any remaining unplaced qubits
644 for (IndexType q = 0; q < nrQubits; ++q)
645 if (qubitGroup[q] < 0) chain.push_back(q);
646
647 assert(chain.size() == static_cast<size_t>(nrQubits));
648
649 // Convert chain to qubitsMap: chain[physPos] = logicalQubit
650 std::vector<long long int> result(nrQubits);
651 for (size_t i = 0; i < chain.size(); ++i)
652 result[chain[i]] = static_cast<long long int>(i);
653
654 return result;
655 };
656
657 auto optMap = buildChain(layerPairs);
658 if (layers.size() <= 2) return optMap;
659 auto optCost = evaluateCost(optMap);
660
661 std::vector<long long int> qubitsMap(nrQubits);
662 std::iota(qubitsMap.begin(), qubitsMap.end(), 0);
663 double tryCost = evaluateCostBounded(qubitsMap, optCost);
664 if (tryCost < optCost) {
665 optCost = tryCost;
666 optMap = qubitsMap;
667 }
668
669 // try some random shuffles as well, in case the heuristic ordering is not
670 // optimal
671 std::mt19937 rng(42);
672 for (int i = 0; i < nrShuffles; ++i) {
673 std::shuffle(qubitsMap.begin(), qubitsMap.end(), rng);
674 tryCost = evaluateCostBounded(qubitsMap, optCost);
675 if (tryCost < optCost) {
676 optCost = tryCost;
677 optMap = qubitsMap;
678 }
679 }
680
681
682 std::uniform_int_distribution<IndexType> qubitDist(0, nrQubits - 1);
683 std::uniform_int_distribution<int> nrSwapsDist(
684 1, std::min<int>(3, static_cast<int>(nrQubits) / 2));
685
686 const int maxNoImprove = std::max(nrShuffles, static_cast<int>(nrQubits));
687 const int maxTotalShuffles = maxNoImprove * 3;
688 int noImproveCount = 0;
689
690 for (int s = 0; s < maxTotalShuffles && noImproveCount < maxNoImprove; ++s) {
691 auto tryMap = optMap;
692 const int nrSwaps = nrSwapsDist(rng);
693 for (int sw = 0; sw < nrSwaps; ++sw) {
694 const IndexType a = qubitDist(rng);
695 IndexType b = qubitDist(rng);
696 while (b == a) b = qubitDist(rng);
697 std::swap(tryMap[a], tryMap[b]);
698 }
699
700 auto cost = evaluateCostBounded(tryMap, optCost);
701 if (cost < optCost) {
702 optMap = tryMap;
703 optCost = cost;
704 noImproveCount = 0;
705 } else {
706 ++noImproveCount;
707 }
708 }
709
710 // 2-opt local search: iteratively swap pairs of positions in the best map
711 // and keep improvements, until no single swap can reduce the cost
712 {
713 auto candidate = optMap;
714 bool improved = true;
715 for (int improvementCount = 0; improved && improvementCount < nrSwaps;
716 ++improvementCount) {
717 improved = false;
718 for (IndexType i = 0; i < nrQubits; ++i) {
719 for (IndexType j = i + 1; j < nrQubits; ++j) {
720 // swap the mapped positions of qubits i and j
721 std::swap(candidate[i], candidate[j]);
722 auto cost = evaluateCostBounded(candidate, optCost);
723 if (cost < optCost) {
724 optMap = candidate;
725 optCost = cost;
726 improved = true;
727 } else {
728 // revert
729 std::swap(candidate[i], candidate[j]);
730 }
731 }
732 }
733 }
734 }
735
736 return optMap;
737 }
738
739 private:
740 void InitQubitsMap() {
741 qubitsMap.resize(getNrQubits());
742 qubitsMapInv.resize(getNrQubits());
743
744 for (IndexType i = 0; i < static_cast<IndexType>(getNrQubits()); ++i)
745 qubitsMapInv[i] = qubitsMap[i] = i;
746
747 totalSwappingCost = 0;
748 if (!currentBondDim.empty())
749 std::fill(currentBondDim.begin(), currentBondDim.end(), 1.0);
750 }
751
752 struct LightweightInitTag {};
753
754 // Lightweight constructor: sets up qubit maps but skips the expensive
755 // SetMaxBondDimension computation. Caller must populate bond arrays.
756 MPSDummySimulator(size_t N, LightweightInitTag)
757 : nrQubits(N) {
758 }
759
760 void SwapQubits(IndexType qubit1, IndexType qubit2) {
761 IndexType realq1 = qubitsMap[qubit1];
762 IndexType realq2 = qubitsMap[qubit2];
763 if (realq1 > realq2) {
764 std::swap(realq1, realq2);
765 std::swap(qubit1, qubit2);
766 }
767
768 if (realq2 - realq1 <= 1) return;
769
770 const IndexType meetPos = ComputeHeuristicMeetPosition(realq1, realq2);
771 SwapQubitsToPosition(qubit1, qubit2, meetPos);
772 }
773
774 // Pick the meeting position with the lowest bond cost (i.e., lowest
775 // current bond dimension), matching the real MPS simulator's
776 // FindBestMeetingPositionLocal behavior.
777 IndexType ComputeHeuristicMeetPosition(IndexType realq1,
778 IndexType realq2) const {
779 assert(realq1 < realq2);
780
781 IndexType bestPos = realq1;
782 double bestCost = bondCost[realq1];
783
784 for (IndexType m = realq1 + 1; m < realq2; ++m) {
785 if (bondCost[m] < bestCost) {
786 bestCost = bondCost[m];
787 bestPos = m;
788 }
789 }
790
791 return bestPos;
792 }
793
794 public:
795 // Swap two logical qubits so they meet at a specified bond position.
796 // meetPosition is the bond index (in real/chain coordinates) where
797 // the two qubits will end up adjacent: one at meetPosition, the other
798 // at meetPosition+1.
799 // meetPosition must be in [min(realq1,realq2), max(realq1,realq2)-1].
801 IndexType meetPosition) {
802 IndexType realq1 = qubitsMap[qubit1];
803 IndexType realq2 = qubitsMap[qubit2];
804 if (realq1 > realq2) {
805 std::swap(realq1, realq2);
806 std::swap(qubit1, qubit2);
807 }
808
809 if (realq2 - realq1 <= 1) return;
810
811 assert(meetPosition >= realq1 && meetPosition < realq2);
812
813 // Move lower qubit (qubit1) rightward from realq1 to meetPosition
814 {
815 IndexType movingReal = realq1;
816 while (movingReal < meetPosition) {
817 const IndexType toReal = movingReal + 1;
818 const IndexType toInv = qubitsMapInv[toReal];
819
820 qubitsMap[toInv] = movingReal;
821 qubitsMapInv[movingReal] = toInv;
822
823 qubitsMap[qubit1] = toReal;
824 qubitsMapInv[toReal] = qubit1;
825
826 totalSwappingCost += bondCost[movingReal];
827 growBondDimension(movingReal, true);
828 movingReal = toReal;
829 }
830 }
831
832 // Move upper qubit (qubit2) leftward from realq2 to meetPosition+1
833 {
834 IndexType movingReal = realq2;
835 while (movingReal > meetPosition + 1) {
836 const IndexType toReal = movingReal - 1;
837 const IndexType toInv = qubitsMapInv[toReal];
838
839 qubitsMap[toInv] = movingReal;
840 qubitsMapInv[movingReal] = toInv;
841
842 qubitsMap[qubit2] = toReal;
843 qubitsMapInv[toReal] = qubit2;
844
845 totalSwappingCost += bondCost[toReal];
846 growBondDimension(toReal, true);
847 movingReal = toReal;
848 }
849 }
850
851 assert(abs(qubitsMap[qubit1] - qubitsMap[qubit2]) == 1);
852 }
853
854 private:
855 size_t nrQubits;
856
857 std::vector<IndexType> qubitsMap;
858 std::vector<IndexType> qubitsMapInv;
859
860 static constexpr size_t physExtent = 2;
861 IndexType maxVirtualExtent = 0;
862 std::vector<double> bondCost;
863 std::vector<double> maxBondDim;
864 std::vector<double> currentBondDim;
865
866 double totalSwappingCost = 0;
867
868 double growthFactorSwap = 1.;
869 double growthFactorGate = 0.7;
870
871 void growBondDimension(IndexType bond, bool swap = true, int schmidtRank = 4) {
872 // the left and right bond dimensions are relevant because:
873 // the initial configuration before applying the swap or other gate is:
874
875 // - O - O -
876 // | |
877
878 // The two physical legs have dimension 2
879 // this is contracted into:
880
881 // ---
882 // -| |-
883 // ---
884 // | |
885
886 // the left and right dimensions stay the same and also the physical legs have dimension 2
887
888 // then the swap or the other gate is applied, getting a result that looks graphically as above, but of course with different values inside the tensor
889 // swap is special, just swaps the values for (0, 1) and (1, 0) in
890 // the physical legs, while other gates can change all values in the tensor
891
892 // then the tensor is reshaped into a matrix, having dimensions 2 * leftDim x 2 * rightNeighborDim on this matrix SVD is applied, to separate out the
893 // qubits tensors again, and the bond dimension is the number of singular
894 // values kept after truncation (if done), or the number of non-zero
895 // singular values if no truncation is done. The bond dimension can be at
896 // most min(2 * min(leftDim, rightNeighborDim), maxBondDim[bond]) and the minimum is obviously 1
897
898
899 const IndexType leftBond = bond - 1;
900 const IndexType rightNeigborBond = bond + 1;
901 const double betweenDim = currentBondDim[bond];
902
903 const double leftDim = leftBond >= 0 ? currentBondDim[leftBond] : 1;
904 const double rightNeighborDim = rightNeigborBond < static_cast<IndexType>(currentBondDim.size()) ? currentBondDim[rightNeigborBond] : 1;
905
906 double newMaxDim = (swap && leftDim == rightNeighborDim) ? betweenDim : 2. * std::min(leftDim, rightNeighborDim);
907 newMaxDim = std::min(newMaxDim, betweenDim * schmidtRank);
908
909 const double growthFactor = swap ? growthFactorSwap : growthFactorGate;
910
911 currentBondDim[bond] = std::min(newMaxDim * growthFactor, maxBondDim[bond]);
912
913 currentBondDim[bond] = std::max(currentBondDim[bond], 1.);
914
915 bondCost[bond] =
916 currentBondDim[bond] * currentBondDim[bond] * currentBondDim[bond];
917 }
918};
919
920} // namespace Simulators
921
922#endif
Circuit class for holding the sequence of operations.
Definition Circuit.h:46
The operation interface.
Definition Operations.h:358
void SetCurrentBondDimensions(const std::vector< double > &dims)
const std::vector< IndexType > & getQubitsMap() const
void setGrowthFactorSwap(double factor)
const std::vector< IndexType > & getQubitsMapInv() const
void ApplyGates(const std::vector< QC::Gates::AppliedGate< MatrixClass > > &gates)
const std::vector< double > & getCurrentBondDimensions() const
void ApplyGate(const QC::Gates::AppliedGate< MatrixClass > &gate)
std::unique_ptr< MPSDummySimulator > Clone() const
void SwapQubitsToPosition(IndexType qubit1, IndexType qubit2, IndexType meetPosition)
void EvaluateMeetingPositionCost(IndexType meetPosition, const std::vector< std::shared_ptr< Circuits::IOperation<> > > &upcomingGates, long long int currentGateIndex, int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost, bool useSameDummy=false)
QC::TensorNetworks::MPSSimulatorInterface::GateClass GateClass
QC::TensorNetworks::MPSSimulatorInterface::MatrixClass MatrixClass
const std::vector< double > & getMaxBondDimensions() const
void ApplyGates(const std::vector< std::shared_ptr< Circuits::IOperation<> > > &gates)
IndexType FindBestMeetingPosition(const std::vector< std::shared_ptr< Circuits::IOperation<> > > &upcomingGates, long long int currentGateIndex, int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost)
std::vector< long long int > ComputeOptimalQubitsMap(const std::vector< std::shared_ptr< Circuits::Circuit<> > > &layers, int nrShuffles=0, int nrSwaps=0)
void ApplyGate(const GateClass &gate, IndexType qubit, IndexType controllingQubit1=0)
void setGrowthFactorGate(double factor)
void SetInitialQubitsMap(const std::vector< long long int > &initialMap)
void SetMaxBondDimension(IndexType val)
void ApplyGate(const std::shared_ptr< Circuits::IOperation<> > &gate)
@ kGate
the usual quantum gate, result stays in simulator's state
Definition Operations.h:28