Maestro 0.3.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 <array>
23#include <cmath>
24#include <cstring>
25#include <deque>
26#include <iostream>
27#include <numeric>
28#include <random>
29
30#include <Eigen/Eigen>
31#include <unsupported/Eigen/CXX11/Tensor>
32
33#include "MPSSimulatorInterface.h"
34#include "QuantumGate.h"
35#include "RoutingConfig.h"
36#include "RoutingGateInfo.h"
37
38namespace Simulators
39{
40
42{
43 public:
44 using IndexType = long long int;
45 using MatrixClass = QC::TensorNetworks::MPSSimulatorInterface::MatrixClass;
46 using GateClass = QC::TensorNetworks::MPSSimulatorInterface::GateClass;
47
48 // operatorChain selects the MPO model: every site carries an operator
49 // (ket and bra legs, physical dimension 4) instead of a qubit state.
50 explicit MPSDummySimulator(size_t N, bool operatorChain = false) : nrQubits(N), maxVirtualExtent(0)
51 {
52 physExtent = operatorChain ? 4 : 2;
53 // MPS: the cost model overrates scrambled layouts for circuits such as
54 // QFT, so only a large predicted gain moves away from the identity. MPO
55 // QFT measures the other way (the searched layouts are really better at
56 // predicted gains of 2-4), so an MPO accepts any improvement.
57 minLayoutGain = operatorChain ? 1. : 4.;
58 InitQubitsMap();
59
61 }
62
63 std::unique_ptr<MPSDummySimulator> Clone() const
64 {
65 auto clone = std::unique_ptr<MPSDummySimulator>(new MPSDummySimulator(nrQubits, LightweightInitTag{}));
66 clone->qubitsMap = qubitsMap;
67 clone->qubitsMapInv = qubitsMapInv;
68 clone->physExtent = physExtent;
69 clone->maxVirtualExtent = maxVirtualExtent;
70 clone->runtimeBondCap = runtimeBondCap;
71 clone->truncationWeight = truncationWeight;
72 clone->swapGatesRelabelQubits = swapGatesRelabelQubits;
73 clone->bondCost = bondCost;
74 clone->maxBondDim = maxBondDim;
75 clone->currentBondDim = currentBondDim;
76
77 clone->totalSwappingCost = totalSwappingCost;
78
79 clone->growthFactorSwap = growthFactorSwap;
80 clone->growthFactorGate = growthFactorGate;
81 clone->minLayoutGain = minLayoutGain;
82
83 return clone;
84 }
85
86 size_t getNrQubits() const
87 {
88 return nrQubits;
89 }
90
91 bool IsOperatorChain() const
92 {
93 return physExtent == 4;
94 }
95
96 void SetSwapGatesRelabelQubits(bool enabled)
97 {
98 if (swapGatesRelabelQubits == enabled)
99 return;
100 swapGatesRelabelQubits = enabled;
102 }
103
104 // Logical SWAP: exchange labels, keeping tensor sites, bonds and cost intact.
105 // SwapQubits/SwapQubitsToPosition model physical routing and remain separate.
107 {
108 if (q1 < 0 || q2 < 0 || q1 >= static_cast<IndexType>(nrQubits) || q2 >= static_cast<IndexType>(nrQubits))
109 throw std::invalid_argument("Qubit index out of bounds");
110 std::swap(qubitsMap[q1], qubitsMap[q2]);
111 qubitsMapInv[qubitsMap[q1]] = q1;
112 qubitsMapInv[qubitsMap[q2]] = q2;
113 }
114
115 double getGrowthFactorSwap() const
116 {
117 return growthFactorSwap;
118 }
119
120 double getGrowthFactorGate() const
121 {
122 return growthFactorGate;
123 }
124
125 static constexpr double kPostSwapCostWeight = 0.25;
126 static constexpr double kPostGateCostWeight = 0.5;
127
128 // Allow two extra rank doublings above the physical cap. This bounded
129 // horizon distinguishes saturated cuts without estimates growing with
130 // register size. Never exceed the exact central-cut rank (4 per MPO site).
131 static IndexType EffectiveLookaheadMaxBondDimension(size_t nrQubits, bool operatorChain, IndexType cap)
132 {
133 if (cap <= 0)
134 return cap;
135 const auto limit = std::numeric_limits<IndexType>::max();
136 const auto headroom = cap > limit / 4 ? limit : 4 * cap;
137 const size_t half = nrQubits / 2;
138 const size_t safeExponent = operatorChain ? 31 : 62;
139 const auto physical = half > safeExponent ? limit : static_cast<IndexType>(1ULL << (half * (operatorChain ? 2 : 1)));
140 return std::max(cap, std::min(headroom, physical));
141 }
142
143 static IndexType EffectiveMapMaxBondDimension(size_t nrQubits, bool operatorChain, IndexType cap)
144 {
145 return EffectiveLookaheadMaxBondDimension(nrQubits, operatorChain, cap);
146 }
147
152
153 // A capped runtime proxy plus a separately weighted overflow penalty.
154 // Weight 1 adds one cap-cubed unit for each cap-sized overflow; weight 0
155 // charges runtime alone. Standalone planners retain the old cubic default.
156 double BondCost(double dimension) const
157 {
158 const double runtimeDim = runtimeBondCap > 0 ? std::min(dimension, static_cast<double>(runtimeBondCap)) : dimension;
159 const double runtime = runtimeDim * runtimeDim * runtimeDim;
160 const double overflow = runtimeBondCap > 0 ? std::max(0., dimension / runtimeBondCap - 1.) : 0.;
161 return runtime + truncationWeight * runtime * overflow;
162 }
163
164 void SetRoutingCostModel(IndexType cap, double weight)
165 {
166 if (cap < 0 || !std::isfinite(weight) || weight < 0.)
167 throw std::invalid_argument("Invalid routing cost model");
168 if (runtimeBondCap == cap && truncationWeight == weight)
169 return;
170 runtimeBondCap = cap;
171 truncationWeight = weight;
172 for (size_t i = 0; i < bondCost.size(); ++i)
173 bondCost[i] = BondCost(currentBondDim[i]);
175 }
176
178 {
179 for (auto &entry : memoCache)
180 entry.valid = false;
181 }
182
183 void setGrowthFactorSwap(double factor)
184 {
185 if (growthFactorSwap == factor)
186 return;
187 growthFactorSwap = factor;
189 }
190
191 void setGrowthFactorGate(double factor)
192 {
193 if (growthFactorGate == factor)
194 return;
195 growthFactorGate = factor;
197 }
198
199 // ComputeOptimalQubitsMap keeps the identity layout unless another layout
200 // is estimated to cost at least this many times less (1: any improvement).
201 // Defaults: 4 for an MPS, 1 for an MPO.
202 double getMinLayoutGain() const
203 {
204 return minLayoutGain;
205 }
206
207 void setMinLayoutGain(double gain)
208 {
209 if (!(gain >= 1.))
210 throw std::invalid_argument("Minimum layout gain must be at least 1");
211 minLayoutGain = gain;
212 }
213
214 void Clear()
215 {
216 InitQubitsMap();
218 }
219
221 {
222 if (nrQubits == 0)
223 {
224 currentBondDim.clear();
225 bondCost.clear();
226 return;
227 }
228 currentBondDim.assign(nrQubits - 1, 1.0);
229 bondCost.assign(nrQubits - 1, 1.0);
230 }
231
233 {
234 if (nrQubits == 0)
235 {
236 if (maxVirtualExtent != val)
237 {
238 maxVirtualExtent = val;
240 }
241 return;
242 }
243
244 const double untruncatedMaxExtent = std::pow(physExtent, nrQubits / 2);
245 IndexType maxVirtualExtentLimit = static_cast<IndexType>(untruncatedMaxExtent);
246
247 if (untruncatedMaxExtent >= std::numeric_limits<IndexType>::max() || std::isnan(untruncatedMaxExtent) || std::isinf(untruncatedMaxExtent))
248 maxVirtualExtentLimit = std::numeric_limits<IndexType>::max() - 1;
249 else if (untruncatedMaxExtent < 2)
250 maxVirtualExtentLimit = 2;
251
252 const IndexType resolvedMaxVirtualExtent = val == 0 ? maxVirtualExtentLimit : std::min(val, maxVirtualExtentLimit);
253 const bool alreadyInitialized = (maxBondDim.size() + 1 == nrQubits && currentBondDim.size() + 1 == nrQubits && bondCost.size() + 1 == nrQubits);
254 if (alreadyInitialized && maxVirtualExtent == resolvedMaxVirtualExtent)
255 return;
256
257 maxVirtualExtent = resolvedMaxVirtualExtent;
259
260 maxBondDim.resize(nrQubits - 1);
261 if (!alreadyInitialized)
262 {
263 currentBondDim.assign(nrQubits - 1, 1.0);
264 bondCost.assign(nrQubits - 1, 1.0);
265 }
266
267 // the checks here are overkill, but better safe than sorry
268 // we're dealing with large values here, overflows are to be expected
269 for (IndexType i = 0; i < static_cast<IndexType>(nrQubits); ++i)
270 {
271 double maxExtent1 = std::pow((double)physExtent, (double)i + 1.);
272 double maxExtent2 = std::pow((double)physExtent, (double)nrQubits - i - 1.);
273
274 if (maxExtent1 >= (double)std::numeric_limits<size_t>::max() || std::isnan(maxExtent1) || std::isinf(maxExtent1))
275 maxExtent1 = (double)std::numeric_limits<size_t>::max() - 1;
276 else if (maxExtent1 < 1)
277 maxExtent1 = 1;
278
279 if (maxExtent2 > (double)std::numeric_limits<size_t>::max() || std::isnan(maxExtent2) || std::isinf(maxExtent2))
280 maxExtent2 = (double)std::numeric_limits<size_t>::max() - 1;
281 else if (maxExtent2 < 1)
282 maxExtent2 = 1;
283
284 size_t maxRightExtent = (size_t)std::min<double>({maxExtent1, maxExtent2, (double)maxVirtualExtent});
285 if (maxRightExtent < 2)
286 maxRightExtent = 2;
287
288 if (i < static_cast<IndexType>(nrQubits) - 1)
289 {
290 maxBondDim[i] = static_cast<double>(maxRightExtent);
291 if (alreadyInitialized)
292 {
293 currentBondDim[i] = std::clamp(currentBondDim[i], 1.0, maxBondDim[i]);
294 bondCost[i] = BondCost(currentBondDim[i]);
295 }
296 }
297 }
298 }
299
300 void print() const
301 {
302 std::cout << "Qubits map: ";
303 for (int q = 0; q < static_cast<int>(qubitsMap.size()); ++q)
304 std::cout << q << "->" << qubitsMap[q] << " ";
305 std::cout << std::endl;
306 }
307
308 void ApplyGate(const QC::Gates::AppliedGate<MatrixClass> &gate)
309 {
310 ApplyGate(gate, gate.getQubit1(), gate.getQubit2());
311 }
312
313 void ApplyGate(const std::shared_ptr<Circuits::IOperation<>> &gate)
314 {
315 if (gate->GetType() != Circuits::OperationType::kGate)
316 return;
317 const auto qbits = gate->AffectedQubits();
318
319 if (qbits.size() == 1)
320 {
321 // 1-qubit gates are no-ops in the dummy simulator (no swap logic)
322 return;
323 }
324 else if (qbits.size() == 2)
325 {
326 static const QC::Gates::SwapGate<MatrixClass> dummySwapGate;
327
328 const auto gateptr = std::dynamic_pointer_cast<Circuits::IQuantumGate<>>(gate);
329 const auto info = std::dynamic_pointer_cast<RoutingGateInfo>(gate);
330 if (info)
331 ApplyTwoQubitFastGate(qbits[0], qbits[1], info->isSwap, info->schmidtRank);
332 else if (gateptr && gateptr->GetGateType() == Circuits::QuantumGateType::kSwapGateType)
333 ApplyGate(dummySwapGate, qbits[0], qbits[1]);
334 else
335 ApplyTwoQubitGate(qbits[0], qbits[1], gateptr ? GateSchmidtRank(*gateptr) : 4.);
336 }
337 else if (qbits.size() == 3 && gate->GetType() == Circuits::OperationType::kGate)
338 {
339 static const QC::Gates::CNOTGate<MatrixClass> dummyControlled2qGate;
340
341 const auto gateptr = std::static_pointer_cast<Circuits::IQuantumGate<>>(gate);
342
343 const size_t q1 = qbits[0]; // control 1
344 const size_t q2 = qbits[1]; // control 2
345 const size_t q3 = qbits[2]; // target
346 if (gateptr->GetGateType() == Circuits::QuantumGateType::kCCXGateType)
347 {
348 ApplyGate(dummyControlled2qGate, q3, q2);
349 ApplyGate(dummyControlled2qGate, q2, q1);
350 ApplyGate(dummyControlled2qGate, q3, q2);
351 ApplyGate(dummyControlled2qGate, q2, q1);
352 ApplyGate(dummyControlled2qGate, q3, q1);
353 }
354 else
355 { // cswap
356 ApplyGate(dummyControlled2qGate, q2, q3);
357 ApplyGate(dummyControlled2qGate, q3, q2);
358 ApplyGate(dummyControlled2qGate, q2, q1);
359 ApplyGate(dummyControlled2qGate, q3, q2);
360 ApplyGate(dummyControlled2qGate, q2, q1);
361 ApplyGate(dummyControlled2qGate, q3, q1);
362 ApplyGate(dummyControlled2qGate, q2, q3);
363 }
364 }
365 }
366
367 void ApplyGate(const GateClass &gate, IndexType qubit, IndexType controllingQubit1 = 0)
368 {
369 if (qubit < 0 || qubit >= static_cast<IndexType>(getNrQubits()))
370 throw std::invalid_argument("Qubit index out of bounds");
371 else if (controllingQubit1 < 0 || controllingQubit1 >= static_cast<IndexType>(getNrQubits()))
372 throw std::invalid_argument("Qubit index out of bounds");
373
374 // for two qubit gates:
375 // if the qubits are not adjacent, apply swap gates until they are
376 // don't forget to update the qubitsMap
377 if (gate.getQubitsNumber() > 1)
378 {
379 if (gate.getQubitsNumber() == 2 && gate.isSwapGate() && swapGatesRelabelQubits)
380 return RelabelQubits(qubit, controllingQubit1);
381 // QCSim gate objects carry no angle: controlled ones count as rank 2,
382 // the others as 4.
383 ApplyTwoQubitGate(qubit, controllingQubit1, gate.isControlled() ? 2. : 4., gate.isSwapGate());
384 }
385 }
386
387 // Effective Schmidt rank of a named or generic two-qubit gate.
388 static double GateSchmidtRank(const Circuits::IQuantumGate<> &gate)
389 {
390 if (gate.GetNumQubits() != 2)
391 return 4.;
392 const auto params = gate.GetParams();
393 return RoutingSchmidtRank(gate.GetGateType(), params.empty() ? 0. : params[0], [&gate] { return gate.GetMatrix(); });
394 }
395
396 // Route two qubits together if needed, then grow the bond between them.
397 void ApplyTwoQubitGate(IndexType qubit, IndexType controllingQubit1, double schmidtRank, bool isSwapGate = false)
398 {
399 if (qubit < 0 || qubit >= static_cast<IndexType>(getNrQubits()))
400 throw std::invalid_argument("Qubit index out of bounds");
401 else if (controllingQubit1 < 0 || controllingQubit1 >= static_cast<IndexType>(getNrQubits()))
402 throw std::invalid_argument("Qubit index out of bounds");
403 IndexType qubit1 = qubitsMap[qubit];
404 IndexType qubit2 = qubitsMap[controllingQubit1];
405
406 if (abs(qubit1 - qubit2) > 1)
407 {
408 SwapQubits(qubit, controllingQubit1);
409 qubit1 = qubitsMap[qubit];
410 qubit2 = qubitsMap[controllingQubit1];
411 assert(abs(qubit1 - qubit2) == 1);
412 }
413
414 // any 2-qubit gate (whether swaps were needed or not) grows the
415 // bond dimension at the bond between the two adjacent qubits.
416 // Charge both the pre-gate contraction cost and a post-growth SVD/reconstruction
417 // cost so immediate entanglement growth and gate contraction overhead at this
418 // bond are reflected even near the end of the lookahead horizon.
419 // The growth factor is the gate's effective Schmidt rank
420 // (EffectiveSchmidtRank): 2 for CX/CZ, 4 for SWAP, near 1 for small angles.
421 RecordAdjacentTwoQubitGate(std::min(qubit1, qubit2), isSwapGate, schmidtRank);
422 }
423
424 void RecordAdjacentTwoQubitGate(IndexType bond, bool isSwapGate, double schmidtRank)
425 {
426 if (bond < 0 || bond >= static_cast<IndexType>(currentBondDim.size()))
427 return;
428 totalSwappingCost += bondCost[bond];
429 growBondDimension(bond, isSwapGate, schmidtRank);
430 totalSwappingCost += kPostGateCostWeight * bondCost[bond];
431 }
432
433 void ApplyGates(const std::vector<QC::Gates::AppliedGate<MatrixClass>> &gates)
434 {
435 for (const auto &gate : gates)
436 ApplyGate(gate);
437 }
438
439 void ApplyGates(const std::vector<std::shared_ptr<Circuits::IOperation<>>> &gates)
440 {
441 for (const auto &gate : gates)
442 ApplyGate(gate);
443 }
444
445 void SetRoutingMap(const std::vector<long long int> &actualMap)
446 {
447 qubitsMap = actualMap;
448 for (size_t i = 0; i < actualMap.size(); ++i)
449 qubitsMapInv[actualMap[i]] = i;
450 }
451
452 void SetInitialQubitsMap(const std::vector<long long int> &initialMap)
453 {
454 SetRoutingMap(initialMap);
455 totalSwappingCost = 0;
457 }
458
459 void SetCurrentBondDimensions(const std::vector<double> &dims)
460 {
461 assert(dims.size() == currentBondDim.size());
462 for (size_t i = 0; i < dims.size(); ++i)
463 currentBondDim[i] = std::min(dims[i], maxBondDim[i]);
464
465 // update bondCost as well, since it depends on the current bond dimensions
466 for (size_t i = 0; i < currentBondDim.size(); ++i)
467 bondCost[i] = BondCost(currentBondDim[i]);
468 }
469
470 const std::vector<double> &getCurrentBondDimensions() const
471 {
472 return currentBondDim;
473 }
474
475 const std::vector<double> &getMaxBondDimensions() const
476 {
477 return maxBondDim;
478 }
479
480 const std::vector<IndexType> &getQubitsMap() const
481 {
482 return qubitsMap;
483 }
484
485 const std::vector<IndexType> &getQubitsMapInv() const
486 {
487 return qubitsMapInv;
488 }
489
490 void setTotalSwappingCost(double cost)
491 {
492 totalSwappingCost = cost;
493 }
494
495 double getTotalSwappingCost() const
496 {
497 return totalSwappingCost;
498 }
499
512
513 // Evaluate the total cost of moving the current gate's qubits to meetPosition,
514 // applying the current 2-qubit gate, and then simulating the next
515 // lookaheadDepth 2-qubit gates from upcomingGates.
516 void EvaluateMeetingPositionCost(IndexType meetPosition, const std::vector<std::shared_ptr<Circuits::IOperation<>>> &upcomingGates,
517 long long int currentGateIndex, int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost,
518 bool useSameDummy = false)
519 {
520 const size_t maxNeeded = static_cast<size_t>(std::max(0, lookaheadDepth)) + 10;
521 std::array<FastGateOp, 128> stackWindow;
522 std::vector<FastGateOp> heapWindow;
523 FastGateOp *windowPtr = stackWindow.data();
524 if (maxNeeded > stackWindow.size())
525 {
526 heapWindow.resize(maxNeeded);
527 windowPtr = heapWindow.data();
528 }
529 const size_t windowSize = ExtractFastGateWindow(upcomingGates, currentGateIndex, maxNeeded, windowPtr);
530 if (windowSize == 0)
531 {
532 if (currentCost < bestCost)
533 bestCost = currentCost;
534 return;
535 }
536
537 EvaluateMeetingPositionCostFast(meetPosition, windowPtr, windowSize, 0, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost,
538 useSameDummy);
539 }
540
541 // Find the meeting position that minimizes the estimated cost of the
542 // current swap + lookahead gates using exact branching for the first
543 // (lookaheadDepth - lookaheadDepthWithHeuristic) steps and a multi-policy
544 // rollout portfolio for the remaining heuristic horizon.
545 // bestCost receives the total accumulated cost for the chosen position.
546 IndexType FindBestMeetingPosition(const std::vector<std::shared_ptr<Circuits::IOperation<>>> &upcomingGates, long long int currentGateIndex,
547 int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost)
548 {
549 if (currentGateIndex < 0 || currentGateIndex >= static_cast<long long int>(upcomingGates.size()))
550 {
551 if (currentCost < bestCost)
552 bestCost = currentCost;
553 return -1;
554 }
555
556 const size_t maxNeeded = static_cast<size_t>(std::max(0, lookaheadDepth)) + 10;
557 std::array<FastGateOp, 128> stackWindow;
558 std::vector<FastGateOp> heapWindow;
559 FastGateOp *windowPtr = stackWindow.data();
560 if (maxNeeded > stackWindow.size())
561 {
562 heapWindow.resize(maxNeeded);
563 windowPtr = heapWindow.data();
564 }
565 const size_t windowSize = ExtractFastGateWindow(upcomingGates, currentGateIndex, maxNeeded, windowPtr);
566 if (windowSize == 0)
567 {
568 if (currentCost < bestCost)
569 bestCost = currentCost;
570 return -1;
571 }
572
573 uint64_t stateHash = 14695981039346656037ULL;
574 auto mixHash = [&](uint64_t v) {
575 stateHash ^= v;
576 stateHash *= 1099511628211ULL;
577 };
578 auto mixDouble = [&](double d) {
579 uint64_t bits = 0;
580 std::memcpy(&bits, &d, sizeof(double));
581 mixHash(bits);
582 };
583
584 mixHash(static_cast<uint64_t>(windowSize));
585 mixHash(static_cast<uint64_t>(lookaheadDepth));
586 mixHash(static_cast<uint64_t>(lookaheadDepthWithHeuristic));
587 mixHash(static_cast<uint64_t>(physExtent));
588 mixHash(static_cast<uint64_t>(maxVirtualExtent));
589 mixHash(static_cast<uint64_t>(swapGatesRelabelQubits));
590 mixDouble(growthFactorSwap);
591 mixDouble(growthFactorGate);
592 for (IndexType q : qubitsMap)
593 mixHash(static_cast<uint64_t>(q));
594 for (double d : currentBondDim)
595 mixDouble(d);
596 for (double c : bondCost)
597 mixDouble(c);
598 for (double m : maxBondDim)
599 mixDouble(m);
600 for (size_t w = 0; w < windowSize; ++w)
601 {
602 const FastGateOp &fg = windowPtr[w];
603 mixHash(static_cast<uint64_t>(fg.q1));
604 mixHash(static_cast<uint64_t>(fg.q2));
605 mixHash(static_cast<uint64_t>(fg.q3));
606 mixHash(static_cast<uint64_t>(fg.numQubits));
607 mixHash(static_cast<uint64_t>(fg.isSwap ? 1 : 0));
608 mixDouble(fg.schmidtRank);
609 }
610
611 MemoEntry &slot = memoCache[(static_cast<size_t>(currentGateIndex) ^ static_cast<size_t>(stateHash)) & (kMemoCacheSize - 1)];
612 if (slot.valid && slot.hash == stateHash)
613 {
614 const double candCost = currentCost + slot.costDelta;
615 if (candCost < bestCost)
616 bestCost = candCost;
617 return slot.bestPos;
618 }
619
620 double localBestCost = std::numeric_limits<double>::infinity();
621 const IndexType bestPos = FindBestMeetingPositionFast(windowPtr, windowSize, 0, lookaheadDepth, lookaheadDepthWithHeuristic, 0.0, localBestCost);
622
623 slot.valid = true;
624 slot.hash = stateHash;
625 slot.bestPos = bestPos;
626 slot.costDelta = localBestCost;
627
628 if (currentCost + localBestCost < bestCost)
629 bestCost = currentCost + localBestCost;
630
631 return bestPos;
632 }
633
634 std::vector<long long int> ComputeOptimalQubitsMap(const std::vector<std::shared_ptr<Circuits::Circuit<>>> &layers, int nrShuffles = 0 /*25*/,
635 int nrSwaps = 0 /*10*/)
636 {
637 const IndexType nrQubits = getNrQubits();
638
639 if (layers.empty() || nrQubits <= 2)
640 return qubitsMap;
641
642 auto evaluateCost = [&, this](const std::vector<IndexType> &candidateMap) -> double {
643 auto saveQubitsMap = qubitsMap;
644 auto saveQubitsMapInv = qubitsMapInv;
645 auto saveCurrentBondDim = currentBondDim;
646 auto saveTotalSwappingCost = totalSwappingCost;
647 auto saveBondCost = bondCost;
648
649 SetInitialQubitsMap(candidateMap);
650
651 for (const auto &layer : layers)
652 ApplyGates(layer->GetOperations());
653 auto cost = getTotalSwappingCost();
654 qubitsMap = std::move(saveQubitsMap);
655 qubitsMapInv = std::move(saveQubitsMapInv);
656 currentBondDim = std::move(saveCurrentBondDim);
657 totalSwappingCost = saveTotalSwappingCost;
658 bondCost = std::move(saveBondCost);
659
660 return cost;
661 };
662
663 auto evaluateCostBounded = [&, this](const std::vector<IndexType> &candidateMap, double bound) -> double {
664 auto saveQubitsMap = qubitsMap;
665 auto saveQubitsMapInv = qubitsMapInv;
666 auto saveCurrentBondDim = currentBondDim;
667 auto saveTotalSwappingCost = totalSwappingCost;
668 auto saveBondCost = bondCost;
669
670 SetInitialQubitsMap(candidateMap);
671
672 for (const auto &layer : layers)
673 {
674 ApplyGates(layer->GetOperations());
675 auto cost = getTotalSwappingCost();
676 if (cost >= bound)
677 {
678 // restore state
679 qubitsMap = std::move(saveQubitsMap);
680 qubitsMapInv = std::move(saveQubitsMapInv);
681 currentBondDim = std::move(saveCurrentBondDim);
682 totalSwappingCost = saveTotalSwappingCost;
683 bondCost = std::move(saveBondCost);
684 return cost;
685 }
686 }
687 auto cost = getTotalSwappingCost();
688 // restore state
689 qubitsMap = std::move(saveQubitsMap);
690 qubitsMapInv = std::move(saveQubitsMapInv);
691 currentBondDim = std::move(saveCurrentBondDim);
692 totalSwappingCost = saveTotalSwappingCost;
693 bondCost = std::move(saveBondCost);
694
695 return cost;
696 };
697
698 // Collect 2-qubit pairs from each layer, preserving layer boundaries
699 struct QubitPair
700 {
701 IndexType q1, q2;
702 };
703
704 std::vector<std::vector<QubitPair>> layerPairs;
705 std::vector<IndexType> initialLabels(nrQubits);
706 std::iota(initialLabels.begin(), initialLabels.end(), 0);
707
708 for (size_t li = 0; li < layers.size(); ++li)
709 {
710 const auto &layer = layers[li];
711 std::vector<QubitPair> lp;
712 for (const auto &op : layer->GetOperations())
713 {
714 auto qbits = op->AffectedQubits();
715 if (qbits.size() >= 2)
716 {
717 if (swapGatesRelabelQubits)
718 {
719 const auto gate = std::dynamic_pointer_cast<Circuits::IQuantumGate<>>(op);
720 const auto info = std::dynamic_pointer_cast<RoutingGateInfo>(op);
721 if (qbits.size() == 2 && ((info && info->isSwap) || (gate && gate->GetGateType() == Circuits::QuantumGateType::kSwapGateType)))
722 {
723 std::swap(initialLabels[qbits[0]], initialLabels[qbits[1]]);
724 continue;
725 }
726 // Express later partners in the labels of the initial
727 // state; a free SWAP itself creates no layout edge.
728 for (auto &q : qbits)
729 q = initialLabels[q];
730 }
731 if (qbits[0] > qbits[1])
732 std::swap(qbits[0], qbits[1]);
733
734 lp.push_back({static_cast<IndexType>(qbits[0]), static_cast<IndexType>(qbits[1])});
735 }
736 }
737 if (!lp.empty())
738 layerPairs.push_back(std::move(lp));
739 }
740 if (layerPairs.empty())
741 return qubitsMap;
742
743 // Build a linear qubit chain from ordered pairs using a group-merging
744 // strategy. Pairs are processed in order (earlier = more important).
745 // - If neither qubit is placed, create a new group [q1, q2].
746 // - If one qubit is already in a group, add the other at the group
747 // end (front or back) that minimizes distance to the existing one.
748 // - If both are in different groups, merge as [gA][gB] or [gB][gA],
749 // whichever places q1 and q2 closer together.
750 // - If both are already in the same group, do nothing.
751 auto buildChain = [&](const std::vector<std::vector<QubitPair>> &orderedLP) -> std::vector<long long int> {
752 std::vector<std::deque<IndexType>> groups;
753 std::vector<int> qubitGroup(nrQubits, -1);
754 std::unordered_set<IndexType> placedQubits;
755
756 for (const auto &lp : orderedLP)
757 {
758 for (const auto &p : lp)
759 {
760 const int g1 = qubitGroup[p.q1];
761 const int g2 = qubitGroup[p.q2];
762
763 if (g1 < 0 && g2 < 0)
764 {
765 // Neither placed: create a new group
766 const int gIdx = static_cast<int>(groups.size());
767 groups.push_back({p.q1, p.q2});
768 qubitGroup[p.q1] = gIdx;
769 qubitGroup[p.q2] = gIdx;
770 placedQubits.insert(p.q1);
771 placedQubits.insert(p.q2);
772 }
773 else if (g1 >= 0 && g2 < 0)
774 {
775 // q1 placed, q2 not: add q2 at the closer end of q1's group
776 auto &grp = groups[g1];
777 size_t idx = 0;
778 for (size_t i = 0; i < grp.size(); ++i)
779 if (grp[i] == p.q1)
780 {
781 idx = i;
782 break;
783 }
784 // front distance: idx + 1, back distance: grp.size() - idx
785 if (idx + 1 <= grp.size() - idx)
786 grp.push_front(p.q2);
787 else
788 grp.push_back(p.q2);
789 qubitGroup[p.q2] = g1;
790 placedQubits.insert(p.q2);
791 }
792 else if (g1 < 0 && g2 >= 0)
793 {
794 // q2 placed, q1 not: add q1 at the closer end of q2's group
795 auto &grp = groups[g2];
796 size_t idx = 0;
797 for (size_t i = 0; i < grp.size(); ++i)
798 if (grp[i] == p.q2)
799 {
800 idx = i;
801 break;
802 }
803 if (idx + 1 <= grp.size() - idx)
804 grp.push_front(p.q1);
805 else
806 grp.push_back(p.q1);
807 qubitGroup[p.q1] = g2;
808 placedQubits.insert(p.q1);
809 }
810 else if (g1 != g2)
811 {
812 // Both in different groups: merge to minimize distance
813 auto &grpA = groups[g1];
814 auto &grpB = groups[g2];
815 size_t idxA = 0, idxB = 0;
816 for (size_t i = 0; i < grpA.size(); ++i)
817 if (grpA[i] == p.q1)
818 {
819 idxA = i;
820 break;
821 }
822 for (size_t i = 0; i < grpB.size(); ++i)
823 if (grpB[i] == p.q2)
824 {
825 idxB = i;
826 break;
827 }
828 // [A][B]: q1 at idxA, q2 at |A|+idxB -> dist = |A|+idxB-idxA
829 // [B][A]: q2 at idxB, q1 at |B|+idxA -> dist = |B|+idxA-idxB
830 const size_t distAB = grpA.size() + idxB - idxA;
831 const size_t distBA = grpB.size() + idxA - idxB;
832
833 int mergedGroup;
834 if (distAB <= distBA)
835 {
836 for (const auto q : grpB)
837 grpA.push_back(q);
838 grpB.clear();
839 mergedGroup = g1;
840 }
841 else
842 {
843 for (const auto q : grpA)
844 grpB.push_back(q);
845 grpA.clear();
846 mergedGroup = g2;
847 }
848 for (const auto q : groups[mergedGroup])
849 qubitGroup[q] = mergedGroup;
850 }
851 // Both in the same group: do nothing
852
853 if (placedQubits.size() == static_cast<size_t>(nrQubits))
854 break; // all qubits placed, can stop processing pairs
855 }
856
857 if (placedQubits.size() == static_cast<size_t>(nrQubits))
858 break; // all qubits placed, can stop processing pairs
859 }
860
861 // Concatenate all non-empty groups
862 std::vector<IndexType> chain;
863 chain.reserve(nrQubits);
864 for (const auto &grp : groups)
865 for (const auto q : grp)
866 chain.push_back(q);
867
868 // Append any remaining unplaced qubits
869 for (IndexType q = 0; q < nrQubits; ++q)
870 if (qubitGroup[q] < 0)
871 chain.push_back(q);
872
873 assert(chain.size() == static_cast<size_t>(nrQubits));
874
875 // Convert chain to qubitsMap: chain[physPos] = logicalQubit
876 std::vector<long long int> result(nrQubits);
877 for (size_t i = 0; i < chain.size(); ++i)
878 result[chain[i]] = static_cast<long long int>(i);
879
880 return result;
881 };
882
883 // Build a time-decayed full-circuit interaction graph across all layers
884 // so deeper gates and multi-layer topology (ladders, 2D grids, islands)
885 // are visible even after all qubits have appeared once. Uses sparse
886 // adjacency lists O(|E|) instead of an O(N^2) dense matrix.
887 struct WeightedEdge
888 {
889 IndexType u;
890 IndexType v;
891 double w;
892 };
893
894 const size_t nQ = static_cast<size_t>(nrQubits);
895 std::vector<std::vector<std::pair<IndexType, double>>> adjList(nQ);
896 auto addEdgeWeight = [&](IndexType u, IndexType v, double w) {
897 auto &listU = adjList[static_cast<size_t>(u)];
898 for (auto &nb : listU)
899 {
900 if (nb.first == v)
901 {
902 nb.second += w;
903 return;
904 }
905 }
906 listU.push_back({v, w});
907 };
908
909 const double decay = std::exp(-2.5 / static_cast<double>(std::max<size_t>(1, layerPairs.size())));
910 double layerWeight = 1.0;
911 for (const auto &lp : layerPairs)
912 {
913 for (const auto &p : lp)
914 {
915 if (p.q1 != p.q2 && p.q1 >= 0 && p.q1 < nrQubits && p.q2 >= 0 && p.q2 < nrQubits)
916 {
917 addEdgeWeight(p.q1, p.q2, layerWeight);
918 addEdgeWeight(p.q2, p.q1, layerWeight);
919 }
920 }
921 layerWeight *= decay;
922 }
923
924 std::vector<WeightedEdge> edges;
925 for (IndexType u = 0; u < nrQubits; ++u)
926 {
927 auto &listU = adjList[static_cast<size_t>(u)];
928 std::sort(listU.begin(), listU.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
929 for (const auto &nb : listU)
930 {
931 if (u < nb.first && nb.second > 0.0)
932 edges.push_back({u, nb.first, nb.second});
933 }
934 }
935 std::sort(edges.begin(), edges.end(), [](const WeightedEdge &a, const WeightedEdge &b) {
936 if (a.w != b.w)
937 return a.w > b.w;
938 return std::tie(a.u, a.v) < std::tie(b.u, b.v);
939 });
940
941 auto getEdgeWeight = [&](IndexType u, IndexType v) -> double {
942 if (u == v)
943 return 0.0;
944 const auto &list = (adjList[u].size() <= adjList[v].size()) ? adjList[u] : adjList[v];
945 const IndexType target = (adjList[u].size() <= adjList[v].size()) ? v : u;
946 auto it =
947 std::lower_bound(list.begin(), list.end(), target, [](const std::pair<IndexType, double> &elem, IndexType val) { return elem.first < val; });
948 return (it != list.end() && it->first == target) ? it->second : 0.0;
949 };
950
951 // Superlinear (convex) swap distance penalty: penalizes long-range spans
952 // more than a pair of medium spans so ladders and 2D grids interleave cleanly.
953 auto pairSwapPenalty = [](IndexType posU, IndexType posV) -> double {
954 const double swaps = static_cast<double>(std::max<IndexType>(0, std::abs(posU - posV) - 1));
955 return swaps * (1.0 + 0.25 * std::sqrt(swaps));
956 };
957
958 auto qubitGraphCost = [&](IndexType u, IndexType posU, const std::vector<IndexType> &pos) -> double {
959 double c = 0.0;
960 for (const auto &nb : adjList[u])
961 c += nb.second * pairSwapPenalty(posU, pos[nb.first]);
962 return c;
963 };
964
965 auto totalGraphCost = [&](const std::vector<IndexType> &pos) -> double {
966 double c = 0.0;
967 for (const auto &e : edges)
968 c += e.w * pairSwapPenalty(pos[e.u], pos[e.v]);
969 return c;
970 };
971
972 // Bounded O(|E| * W) 2-opt swap + incremental adjacent-slide insertion
973 // local search on the convex interaction graph cost.
974 auto refineGraphLayout = [&](std::vector<IndexType> pos) -> std::vector<IndexType> {
975 if (edges.empty() || nrQubits <= 2)
976 return pos;
977
978 // Early exit when all interacting pairs are already adjacent.
979 if (totalGraphCost(pos) <= 1e-12)
980 return pos;
981
982 std::vector<IndexType> order(nQ);
983 for (IndexType q = 0; q < nrQubits; ++q)
984 order[pos[q]] = q;
985
986 const IndexType maxWindow = std::clamp<IndexType>(static_cast<IndexType>(512 / std::max<size_t>(1, nQ / 16)), 8, 48);
987 const int maxIter = (nQ > 64) ? 5 : 12;
988 std::vector<IndexType> candPositions;
989 candPositions.reserve(static_cast<size_t>(maxWindow) + 16);
990
991 for (int iter = 0; iter < maxIter; ++iter)
992 {
993 bool improved = false;
994 for (IndexType p1 = 0; p1 < nrQubits; ++p1)
995 {
996 IndexType u = order[p1];
997 if (qubitGraphCost(u, p1, pos) <= 1e-12)
998 continue;
999
1000 candPositions.clear();
1001 const IndexType p2Max = std::min<IndexType>(nrQubits - 1, p1 + maxWindow);
1002 for (IndexType p2 = p1 + 1; p2 <= p2Max; ++p2)
1003 candPositions.push_back(p2);
1004 for (const auto &nb : adjList[u])
1005 {
1006 const IndexType np = pos[nb.first];
1007 for (IndexType delta = -1; delta <= 1; ++delta)
1008 {
1009 const IndexType p2 = np + delta;
1010 if (p2 > p1 + maxWindow && p2 < nrQubits)
1011 candPositions.push_back(p2);
1012 }
1013 }
1014
1015 for (IndexType p2 : candPositions)
1016 {
1017 u = order[p1];
1018 const IndexType v = order[p2];
1019 const double wuv = getEdgeWeight(u, v);
1020 const double pairTerm = wuv * pairSwapPenalty(p1, p2);
1021 const double before = qubitGraphCost(u, p1, pos) + qubitGraphCost(v, p2, pos) - pairTerm;
1022
1023 pos[u] = p2;
1024 pos[v] = p1;
1025 const double after = qubitGraphCost(u, p2, pos) + qubitGraphCost(v, p1, pos) - pairTerm;
1026
1027 if (after + 1e-9 < before)
1028 {
1029 order[p1] = v;
1030 order[p2] = u;
1031 improved = true;
1032 }
1033 else
1034 {
1035 pos[u] = p1;
1036 pos[v] = p2;
1037 }
1038 }
1039 }
1040
1041 // Incremental single-qubit insertion via adjacent-swap sliding:
1042 // each 1-slot shift only changes incident edge distances of the two
1043 // swapped qubits, avoiding full-graph rescans and vector copies.
1044 auto swapAdjacentPositions = [&](IndexType pA, IndexType pB) -> double {
1045 const IndexType qA = order[pA];
1046 const IndexType qB = order[pB];
1047 // Note: the edge (qA, qB) has |pA - pB| == 1 both before and after,
1048 // where pairSwapPenalty is 0.
1049 const double before = qubitGraphCost(qA, pA, pos) + qubitGraphCost(qB, pB, pos);
1050 pos[qA] = pB;
1051 pos[qB] = pA;
1052 order[pA] = qB;
1053 order[pB] = qA;
1054 const double after = qubitGraphCost(qA, pB, pos) + qubitGraphCost(qB, pA, pos);
1055 return after - before;
1056 };
1057
1058 for (IndexType fromP = 0; fromP < nrQubits; ++fromP)
1059 {
1060 const IndexType q = order[fromP];
1061 if (qubitGraphCost(q, fromP, pos) <= 1e-12)
1062 continue;
1063
1064 const IndexType maxRight = std::min<IndexType>(nrQubits - 1, fromP + maxWindow);
1065 const IndexType minLeft = std::max<IndexType>(0, fromP - maxWindow);
1066
1067 IndexType bestP = fromP;
1068 double bestDelta = 0.0;
1069 double cumDelta = 0.0;
1070
1071 for (IndexType curP = fromP; curP < maxRight; ++curP)
1072 {
1073 cumDelta += swapAdjacentPositions(curP, curP + 1);
1074 if (cumDelta + 1e-9 < bestDelta)
1075 {
1076 bestDelta = cumDelta;
1077 bestP = curP + 1;
1078 }
1079 }
1080 for (IndexType curP = maxRight; curP > fromP; --curP)
1081 swapAdjacentPositions(curP, curP - 1);
1082
1083 cumDelta = 0.0;
1084 for (IndexType curP = fromP; curP > minLeft; --curP)
1085 {
1086 cumDelta += swapAdjacentPositions(curP, curP - 1);
1087 if (cumDelta + 1e-9 < bestDelta)
1088 {
1089 bestDelta = cumDelta;
1090 bestP = curP - 1;
1091 }
1092 }
1093 for (IndexType curP = minLeft; curP < bestP; ++curP)
1094 swapAdjacentPositions(curP, curP + 1);
1095
1096 if (bestP != fromP)
1097 improved = true;
1098 }
1099
1100 if (!improved || totalGraphCost(pos) <= 1e-12)
1101 break;
1102 }
1103 return pos;
1104 };
1105
1106 // Multi-layer weighted group merging with 4-way orientation selection,
1107 // using sparse adjacency lookups O(|E|).
1108 auto buildWeightedChain = [&]() -> std::vector<IndexType> {
1109 std::vector<std::deque<IndexType>> groups;
1110 std::vector<int> qubitGroup(nrQubits, -1);
1111 std::vector<IndexType> posInGroup(nrQubits, 0);
1112
1113 auto refreshGroupPositions = [&](int gIdx) {
1114 const auto &grp = groups[gIdx];
1115 for (size_t i = 0; i < grp.size(); ++i)
1116 posInGroup[grp[i]] = static_cast<IndexType>(i);
1117 };
1118
1119 for (const auto &e : edges)
1120 {
1121 const int g1 = qubitGroup[e.u];
1122 const int g2 = qubitGroup[e.v];
1123 if (g1 < 0 && g2 < 0)
1124 {
1125 const int gIdx = static_cast<int>(groups.size());
1126 groups.push_back({e.u, e.v});
1127 qubitGroup[e.u] = gIdx;
1128 qubitGroup[e.v] = gIdx;
1129 posInGroup[e.u] = 0;
1130 posInGroup[e.v] = 1;
1131 }
1132 else if (g1 >= 0 && g2 < 0)
1133 {
1134 auto &grp = groups[g1];
1135 const IndexType gSize = static_cast<IndexType>(grp.size());
1136 double costFront = 0.0;
1137 double costBack = 0.0;
1138 for (const auto &nb : adjList[e.v])
1139 {
1140 if (qubitGroup[nb.first] == g1)
1141 {
1142 const IndexType idx = posInGroup[nb.first];
1143 costFront += nb.second * pairSwapPenalty(0, idx + 1);
1144 costBack += nb.second * pairSwapPenalty(gSize, idx);
1145 }
1146 }
1147 if (costFront <= costBack)
1148 {
1149 grp.push_front(e.v);
1150 qubitGroup[e.v] = g1;
1151 refreshGroupPositions(g1);
1152 }
1153 else
1154 {
1155 posInGroup[e.v] = gSize;
1156 grp.push_back(e.v);
1157 qubitGroup[e.v] = g1;
1158 }
1159 }
1160 else if (g1 < 0 && g2 >= 0)
1161 {
1162 auto &grp = groups[g2];
1163 const IndexType gSize = static_cast<IndexType>(grp.size());
1164 double costFront = 0.0;
1165 double costBack = 0.0;
1166 for (const auto &nb : adjList[e.u])
1167 {
1168 if (qubitGroup[nb.first] == g2)
1169 {
1170 const IndexType idx = posInGroup[nb.first];
1171 costFront += nb.second * pairSwapPenalty(0, idx + 1);
1172 costBack += nb.second * pairSwapPenalty(gSize, idx);
1173 }
1174 }
1175 if (costFront <= costBack)
1176 {
1177 grp.push_front(e.u);
1178 qubitGroup[e.u] = g2;
1179 refreshGroupPositions(g2);
1180 }
1181 else
1182 {
1183 posInGroup[e.u] = gSize;
1184 grp.push_back(e.u);
1185 qubitGroup[e.u] = g2;
1186 }
1187 }
1188 else if (g1 != g2)
1189 {
1190 // Merge smaller group into larger group to keep position updates O(N log N)
1191 const int gA = (groups[g1].size() >= groups[g2].size()) ? g1 : g2;
1192 const int gB = (gA == g1) ? g2 : g1;
1193 auto &grpA = groups[gA];
1194 auto &grpB = groups[gB];
1195 const IndexType sizeA = static_cast<IndexType>(grpA.size());
1196 const IndexType sizeB = static_cast<IndexType>(grpB.size());
1197
1198 std::array<double, 4> orientCosts{0.0, 0.0, 0.0, 0.0};
1199 for (IndexType ib = 0; ib < sizeB; ++ib)
1200 {
1201 const IndexType qb = grpB[ib];
1202 for (const auto &nb : adjList[qb])
1203 {
1204 if (qubitGroup[nb.first] == gA)
1205 {
1206 const IndexType ia = posInGroup[nb.first];
1207 for (int flipA = 0; flipA < 2; ++flipA)
1208 {
1209 const IndexType effA = flipA ? (sizeA - 1 - ia) : ia;
1210 for (int flipB = 0; flipB < 2; ++flipB)
1211 {
1212 const IndexType effB = sizeA + (flipB ? (sizeB - 1 - ib) : ib);
1213 orientCosts[flipA * 2 + flipB] += nb.second * pairSwapPenalty(effA, effB);
1214 }
1215 }
1216 }
1217 }
1218 }
1219
1220 int bestOrient = 0;
1221 for (int k = 1; k < 4; ++k)
1222 if (orientCosts[k] < orientCosts[bestOrient])
1223 bestOrient = k;
1224
1225 if (bestOrient & 2)
1226 std::reverse(grpA.begin(), grpA.end());
1227 if (bestOrient & 1)
1228 std::reverse(grpB.begin(), grpB.end());
1229 for (const auto q : grpB)
1230 {
1231 grpA.push_back(q);
1232 qubitGroup[q] = gA;
1233 }
1234 grpB.clear();
1235 refreshGroupPositions(gA);
1236 }
1237 }
1238
1239 std::vector<IndexType> chain;
1240 chain.reserve(nQ);
1241 for (const auto &grp : groups)
1242 for (const auto q : grp)
1243 chain.push_back(q);
1244 for (IndexType q = 0; q < nrQubits; ++q)
1245 if (qubitGroup[q] < 0)
1246 chain.push_back(q);
1247
1248 std::vector<IndexType> result(nQ);
1249 for (size_t i = 0; i < chain.size(); ++i)
1250 result[chain[i]] = static_cast<IndexType>(i);
1251 return result;
1252 };
1253
1254 // Spectral Fiedler vector ordering of the time-decayed interaction graph
1255 auto buildSpectralChain = [&]() -> std::vector<IndexType> {
1256 std::vector<double> deg(nQ, 0.0);
1257 for (size_t u = 0; u < nQ; ++u)
1258 for (const auto &nb : adjList[u])
1259 deg[u] += nb.second;
1260
1261 std::vector<double> v0(nQ, 0.0);
1262 double v0Norm2 = 0.0;
1263 for (size_t u = 0; u < nQ; ++u)
1264 {
1265 v0[u] = std::sqrt(std::max(deg[u], 1e-12));
1266 v0Norm2 += v0[u] * v0[u];
1267 }
1268 const double invV0Norm = 1.0 / std::sqrt(std::max(v0Norm2, 1e-24));
1269 for (size_t u = 0; u < nQ; ++u)
1270 v0[u] *= invV0Norm;
1271
1272 std::vector<double> x(nQ);
1273 for (size_t u = 0; u < nQ; ++u)
1274 x[u] = static_cast<double>(u) - 0.5 * static_cast<double>(nQ - 1);
1275
1276 for (int iter = 0; iter < 40; ++iter)
1277 {
1278 double dot0 = 0.0;
1279 for (size_t u = 0; u < nQ; ++u)
1280 dot0 += x[u] * v0[u];
1281 for (size_t u = 0; u < nQ; ++u)
1282 x[u] -= dot0 * v0[u];
1283
1284 std::vector<double> y(nQ, 0.0);
1285 for (size_t u = 0; u < nQ; ++u)
1286 {
1287 y[u] = 0.5 * x[u];
1288 if (deg[u] > 1e-12)
1289 {
1290 const double invSqrtDu = 1.0 / std::sqrt(deg[u]);
1291 for (const auto &nb : adjList[u])
1292 if (deg[nb.first] > 1e-12)
1293 y[u] += 0.5 * nb.second * invSqrtDu * (x[nb.first] / std::sqrt(deg[nb.first]));
1294 }
1295 }
1296
1297 dot0 = 0.0;
1298 for (size_t u = 0; u < nQ; ++u)
1299 dot0 += y[u] * v0[u];
1300 double norm2 = 0.0;
1301 for (size_t u = 0; u < nQ; ++u)
1302 {
1303 y[u] -= dot0 * v0[u];
1304 norm2 += y[u] * y[u];
1305 }
1306 if (norm2 < 1e-24)
1307 break;
1308 const double invNorm = 1.0 / std::sqrt(norm2);
1309 for (size_t u = 0; u < nQ; ++u)
1310 x[u] = y[u] * invNorm;
1311 }
1312
1313 // Precompute quantized integer keys so comparator is a strict weak ordering (transitive)
1314 std::vector<int64_t> quantKey(nQ, 0);
1315 for (size_t u = 0; u < nQ; ++u)
1316 {
1317 const double coord = x[u] / std::sqrt(std::max(deg[u], 1e-12));
1318 quantKey[u] = std::isfinite(coord) ? static_cast<int64_t>(std::llround(coord * 1e9)) : 0;
1319 }
1320
1321 std::vector<IndexType> order(nQ);
1322 std::iota(order.begin(), order.end(), 0);
1323 std::sort(order.begin(), order.end(), [&](IndexType a, IndexType b) { return std::tie(quantKey[a], a) < std::tie(quantKey[b], b); });
1324
1325 std::vector<IndexType> result(nQ);
1326 for (size_t i = 0; i < nQ; ++i)
1327 result[order[i]] = static_cast<IndexType>(i);
1328 return result;
1329 };
1330
1331 // The cost model is a rough proxy: for some circuits (QFT, for one) it
1332 // rates scrambled layouts a few times cheaper than the identity, while
1333 // they can run far slower. Leave the identity only for a large predicted
1334 // gain; a circuit whose labels really are scrambled relative to its
1335 // interactions shows gains of orders of magnitude.
1336 std::vector<long long int> identityMap(nrQubits);
1337 std::iota(identityMap.begin(), identityMap.end(), 0);
1338 auto optMap = buildChain(layerPairs);
1339 auto optCost = evaluateCost(optMap);
1340 const double identityCost = optMap == identityMap ? optCost : evaluateCost(identityMap);
1341 if (identityCost < optCost)
1342 {
1343 optCost = identityCost;
1344 optMap = identityMap;
1345 }
1346
1347 // The graph candidates come from a local search whose cost grows with the
1348 // qubits' degree. On a dense interaction graph (average degree above 8:
1349 // QFT, quantum volume) no line ordering keeps the interacting pairs close,
1350 // the search dominates the layout time (about 40 ms for a 64-qubit QFT)
1351 // and its candidates did not win, so it is skipped there.
1352 const bool denseGraph = edges.size() > 4 * nQ;
1353 if (!denseGraph && totalGraphCost(optMap) > 1e-12)
1354 {
1355 std::vector<std::vector<IndexType>> graphCandidates;
1356 graphCandidates.push_back(refineGraphLayout(identityMap));
1357 graphCandidates.push_back(refineGraphLayout(optMap));
1358 if (layers.size() > 2)
1359 {
1360 graphCandidates.push_back(refineGraphLayout(buildWeightedChain()));
1361 graphCandidates.push_back(refineGraphLayout(buildSpectralChain()));
1362 }
1363
1364 for (const auto &cand : graphCandidates)
1365 {
1366 if (cand == identityMap || cand == optMap)
1367 continue;
1368 const double cost = evaluateCostBounded(cand, optCost);
1369 if (cost < optCost)
1370 {
1371 optCost = cost;
1372 optMap = cand;
1373 }
1374 }
1375 }
1376
1377 if (layers.size() <= 2)
1378 return optMap != identityMap && identityCost < minLayoutGain * optCost ? identityMap : optMap;
1379
1380 // nrShuffles full random shuffles, then a perturbation search (1-3 random
1381 // swaps per try) that always runs: MPO layouts depend on it, and gating it
1382 // on nrShuffles (0 by default) left MPO QFT with worse layouts.
1383 {
1384 std::vector<long long int> trialMap = identityMap;
1385 std::mt19937 rng(42);
1386 for (int i = 0; i < nrShuffles; ++i)
1387 {
1388 std::shuffle(trialMap.begin(), trialMap.end(), rng);
1389 const double tryCost = evaluateCostBounded(trialMap, optCost);
1390 if (tryCost < optCost)
1391 {
1392 optCost = tryCost;
1393 optMap = trialMap;
1394 }
1395 }
1396
1397 std::uniform_int_distribution<IndexType> qubitDist(0, nrQubits - 1);
1398 std::uniform_int_distribution<int> nrSwapsDist(1, std::min<int>(3, static_cast<int>(nrQubits) / 2));
1399
1400 const int maxNoImprove = std::max(nrShuffles, static_cast<int>(nrQubits));
1401 const int maxTotalShuffles = maxNoImprove * 3;
1402 int noImproveCount = 0;
1403
1404 for (int s = 0; s < maxTotalShuffles && noImproveCount < maxNoImprove; ++s)
1405 {
1406 auto tryMap = optMap;
1407 const int swapsCount = nrSwapsDist(rng);
1408 for (int sw = 0; sw < swapsCount; ++sw)
1409 {
1410 const IndexType a = qubitDist(rng);
1411 IndexType b = qubitDist(rng);
1412 while (b == a)
1413 b = qubitDist(rng);
1414 std::swap(tryMap[a], tryMap[b]);
1415 }
1416
1417 const auto cost = evaluateCostBounded(tryMap, optCost);
1418 if (cost < optCost)
1419 {
1420 optMap = tryMap;
1421 optCost = cost;
1422 noImproveCount = 0;
1423 }
1424 else
1425 {
1426 ++noImproveCount;
1427 }
1428 }
1429 }
1430
1431 // Optional explicit 2-opt local search in MPSDummySimulator when nrSwaps > 0
1432 if (nrSwaps > 0)
1433 {
1434 auto candidate = optMap;
1435 bool improved = true;
1436 for (int improvementCount = 0; improved && improvementCount < nrSwaps; ++improvementCount)
1437 {
1438 improved = false;
1439 for (IndexType i = 0; i < nrQubits; ++i)
1440 {
1441 for (IndexType j = i + 1; j < nrQubits; ++j)
1442 {
1443 std::swap(candidate[i], candidate[j]);
1444 const auto cost = evaluateCostBounded(candidate, optCost);
1445 if (cost < optCost)
1446 {
1447 optMap = candidate;
1448 optCost = cost;
1449 improved = true;
1450 }
1451 else
1452 {
1453 std::swap(candidate[i], candidate[j]);
1454 }
1455 }
1456 }
1457 }
1458 }
1459
1460 if (optMap != identityMap && identityCost < minLayoutGain * optCost)
1461 return identityMap;
1462 return optMap;
1463 }
1464
1465 private:
1466 void InitQubitsMap()
1467 {
1468 qubitsMap.resize(getNrQubits());
1469 qubitsMapInv.resize(getNrQubits());
1470
1471 for (IndexType i = 0; i < static_cast<IndexType>(getNrQubits()); ++i)
1472 qubitsMapInv[i] = qubitsMap[i] = i;
1473
1474 totalSwappingCost = 0;
1475 if (!currentBondDim.empty())
1476 std::fill(currentBondDim.begin(), currentBondDim.end(), 1.0);
1477 if (!bondCost.empty())
1478 std::fill(bondCost.begin(), bondCost.end(), 1.0);
1479 }
1480
1481 struct LightweightInitTag
1482 {
1483 };
1484
1485 // Lightweight constructor: sets up qubit maps but skips the expensive
1486 // SetMaxBondDimension computation. Caller must populate bond arrays.
1487 MPSDummySimulator(size_t N, LightweightInitTag) : nrQubits(N)
1488 {
1489 }
1490
1491 void SwapQubits(IndexType qubit1, IndexType qubit2)
1492 {
1493 IndexType realq1 = qubitsMap[qubit1];
1494 IndexType realq2 = qubitsMap[qubit2];
1495 if (realq1 > realq2)
1496 {
1497 std::swap(realq1, realq2);
1498 std::swap(qubit1, qubit2);
1499 }
1500
1501 if (realq2 - realq1 <= 1)
1502 return;
1503
1504 const IndexType meetPos = ComputeHeuristicMeetPosition(realq1, realq2);
1505 SwapQubitsToPosition(qubit1, qubit2, meetPos);
1506 }
1507
1508 // Pick the meeting position with the lowest bond cost (the lowest current
1509 // bond dimension) between the two qubits. QCSim's own local heuristic now
1510 // uses a cut-preserving score, but this path routes every gate itself, and
1511 // here the plain rule works better both as a rollout policy (it adds a route
1512 // the other cost-based policies don't) and in the layout search's cost model.
1513 IndexType ComputeHeuristicMeetPosition(IndexType realq1, IndexType realq2) const
1514 {
1515 assert(realq1 < realq2);
1516
1517 IndexType bestPos = realq1;
1518 double bestCost = bondCost[realq1];
1519
1520 for (IndexType m = realq1 + 1; m < realq2; ++m)
1521 {
1522 if (bondCost[m] < bestCost)
1523 {
1524 bestCost = bondCost[m];
1525 bestPos = m;
1526 }
1527 }
1528
1529 return bestPos;
1530 }
1531
1532 // An MPO site has physical dimension 4, and a gate acts on it as
1533 // rho -> U rho U^dagger, whose operator Schmidt rank is the square of the
1534 // gate's (the swap special case still holds, it only relabels the sites).
1535 // Below rank 2 (small-angle gates) the result is interpolated linearly
1536 // between rank 1, which leaves the bond unchanged, and rank 2, so the
1537 // calibrated growth factors still apply unchanged to integer ranks.
1538 double EstimateGrownBondDimension(IndexType bond, double leftDim, double betweenDim, double rightNeighborDim, bool swap, double schmidtRank) const
1539 {
1540 const double rank = IsOperatorChain() ? schmidtRank * schmidtRank : schmidtRank;
1541 auto grownAt = [&](double r) {
1542 double newMaxDim = (swap && leftDim == rightNeighborDim) ? betweenDim : static_cast<double>(physExtent) * std::min(leftDim, rightNeighborDim);
1543 newMaxDim = std::min(newMaxDim, betweenDim * r);
1544 const double growthFactor = swap ? growthFactorSwap : growthFactorGate;
1545 return std::max(std::min(newMaxDim * growthFactor, maxBondDim[bond]), 1.);
1546 };
1547 if (rank >= 2.)
1548 return grownAt(rank);
1549 return betweenDim + (std::max(rank, 1.) - 1.) * (grownAt(2.) - betweenDim);
1550 }
1551
1552 // Anchor the qubit surrounded by higher bond cost so the less-entangled
1553 // qubit travels across the cheaper region instead of dragging a heavy
1554 // entangled cluster across the chain.
1555 IndexType ComputeAnchorHeavierMeetPosition(IndexType realq1, IndexType realq2) const
1556 {
1557 assert(realq1 < realq2);
1558
1559 const double leftOuter = (realq1 > 0) ? bondCost[realq1 - 1] : 0.0;
1560 const double rightOuter = (realq2 < static_cast<IndexType>(bondCost.size())) ? bondCost[realq2] : 0.0;
1561 const double c1 = std::max(leftOuter, bondCost[realq1]);
1562 const double c2 = std::max(rightOuter, bondCost[realq2 - 1]);
1563
1564 if (c1 > c2)
1565 return realq1;
1566 if (c2 > c1)
1567 return realq2 - 1;
1568
1569 return (realq1 <= static_cast<IndexType>(nrQubits) - 1 - realq2) ? realq1 : (realq2 - 1);
1570 }
1571
1572 struct FastGateOp
1573 {
1574 IndexType q1 = 0;
1575 IndexType q2 = 0;
1576 IndexType q3 = 0;
1577 uint8_t numQubits = 2;
1578 bool isSwap = false;
1579 double schmidtRank = 4;
1580 };
1581
1582 static constexpr size_t kMaxStackQubits = 64;
1583
1584 struct FastStateSnapshot
1585 {
1586 std::array<IndexType, kMaxStackQubits> qMap;
1587 std::array<IndexType, kMaxStackQubits> qMapInv;
1588 std::array<double, kMaxStackQubits> curBondDim;
1589 std::array<double, kMaxStackQubits> bCost;
1590 std::vector<IndexType> heapMap;
1591 std::vector<double> heapBonds;
1592 double totalCost = 0.0;
1593
1594 void SaveFrom(const MPSDummySimulator &sim)
1595 {
1596 const size_t n = sim.nrQubits;
1597 const size_t nb = (n > 0) ? (n - 1) : 0;
1598 totalCost = sim.totalSwappingCost;
1599 if (n <= kMaxStackQubits)
1600 {
1601 std::memcpy(qMap.data(), sim.qubitsMap.data(), n * sizeof(IndexType));
1602 std::memcpy(qMapInv.data(), sim.qubitsMapInv.data(), n * sizeof(IndexType));
1603 if (nb > 0)
1604 {
1605 std::memcpy(curBondDim.data(), sim.currentBondDim.data(), nb * sizeof(double));
1606 std::memcpy(bCost.data(), sim.bondCost.data(), nb * sizeof(double));
1607 }
1608 }
1609 else
1610 {
1611 heapMap.resize(2 * n);
1612 std::memcpy(heapMap.data(), sim.qubitsMap.data(), n * sizeof(IndexType));
1613 std::memcpy(heapMap.data() + n, sim.qubitsMapInv.data(), n * sizeof(IndexType));
1614 heapBonds.resize(2 * nb);
1615 if (nb > 0)
1616 {
1617 std::memcpy(heapBonds.data(), sim.currentBondDim.data(), nb * sizeof(double));
1618 std::memcpy(heapBonds.data() + nb, sim.bondCost.data(), nb * sizeof(double));
1619 }
1620 }
1621 }
1622
1623 void RestoreTo(MPSDummySimulator &sim) const
1624 {
1625 const size_t n = sim.nrQubits;
1626 const size_t nb = (n > 0) ? (n - 1) : 0;
1627 sim.totalSwappingCost = totalCost;
1628 if (n <= kMaxStackQubits)
1629 {
1630 std::memcpy(sim.qubitsMap.data(), qMap.data(), n * sizeof(IndexType));
1631 std::memcpy(sim.qubitsMapInv.data(), qMapInv.data(), n * sizeof(IndexType));
1632 if (nb > 0)
1633 {
1634 std::memcpy(sim.currentBondDim.data(), curBondDim.data(), nb * sizeof(double));
1635 std::memcpy(sim.bondCost.data(), bCost.data(), nb * sizeof(double));
1636 }
1637 }
1638 else
1639 {
1640 std::memcpy(sim.qubitsMap.data(), heapMap.data(), n * sizeof(IndexType));
1641 std::memcpy(sim.qubitsMapInv.data(), heapMap.data() + n, n * sizeof(IndexType));
1642 if (nb > 0)
1643 {
1644 std::memcpy(sim.currentBondDim.data(), heapBonds.data(), nb * sizeof(double));
1645 std::memcpy(sim.bondCost.data(), heapBonds.data() + nb, nb * sizeof(double));
1646 }
1647 }
1648 }
1649 };
1650
1651 size_t ExtractFastGateWindow(const std::vector<std::shared_ptr<Circuits::IOperation<>>> &upcomingGates, long long int startGateIndex, size_t maxCount,
1652 FastGateOp *outOps) const
1653 {
1654 size_t count = 0;
1655 if (startGateIndex < 0)
1656 return 0;
1657
1658 for (long long int idx = startGateIndex; idx < static_cast<long long int>(upcomingGates.size()) && count < maxCount; ++idx)
1659 {
1660 const auto &op = upcomingGates[idx];
1661 if (!op || op->GetType() != Circuits::OperationType::kGate)
1662 break;
1663 const auto qbits = op->AffectedQubits();
1664 if (qbits.size() < 2)
1665 continue;
1666
1667 FastGateOp fg;
1668 fg.q1 = static_cast<IndexType>(qbits[0]);
1669 fg.q2 = static_cast<IndexType>(qbits[1]);
1670 fg.numQubits = static_cast<uint8_t>(qbits.size());
1671 fg.isSwap = false;
1672 fg.schmidtRank = 4;
1673
1674 if (qbits.size() == 2)
1675 {
1676 const auto gateptr = std::dynamic_pointer_cast<Circuits::IQuantumGate<>>(op);
1677 const auto info = std::dynamic_pointer_cast<RoutingGateInfo>(op);
1678 if (info)
1679 {
1680 fg.isSwap = info->isSwap;
1681 fg.schmidtRank = info->schmidtRank;
1682 }
1683 else if (gateptr && gateptr->GetGateType() == Circuits::QuantumGateType::kSwapGateType)
1684 {
1685 fg.isSwap = true;
1686 fg.schmidtRank = 4;
1687 }
1688 else if (gateptr)
1689 {
1690 fg.schmidtRank = GateSchmidtRank(*gateptr);
1691 }
1692 }
1693 else if (qbits.size() == 3)
1694 {
1695 fg.q3 = static_cast<IndexType>(qbits[2]);
1696 const auto gateptr = std::static_pointer_cast<Circuits::IQuantumGate<>>(op);
1697 fg.isSwap = (gateptr->GetGateType() != Circuits::QuantumGateType::kCCXGateType);
1698 fg.schmidtRank = 2;
1699 }
1700
1701 outOps[count++] = fg;
1702 }
1703
1704 return count;
1705 }
1706
1707 void ApplyTwoQubitFastGate(IndexType qubit, IndexType controllingQubit1, bool isSwap, double schmidtRank)
1708 {
1709 if (isSwap && swapGatesRelabelQubits)
1710 return RelabelQubits(qubit, controllingQubit1);
1711 IndexType qubit1 = qubitsMap[qubit];
1712 IndexType qubit2 = qubitsMap[controllingQubit1];
1713
1714 if (abs(qubit1 - qubit2) > 1)
1715 {
1716 SwapQubits(qubit, controllingQubit1);
1717 qubit1 = qubitsMap[qubit];
1718 qubit2 = qubitsMap[controllingQubit1];
1719 }
1720
1721 const IndexType bond = std::min(qubit1, qubit2);
1722 totalSwappingCost += bondCost[bond];
1723 growBondDimension(bond, isSwap, schmidtRank);
1724 totalSwappingCost += kPostGateCostWeight * bondCost[bond];
1725 }
1726
1727 void ApplyFastGate(const FastGateOp &g)
1728 {
1729 if (g.numQubits == 2)
1730 {
1731 ApplyTwoQubitFastGate(g.q1, g.q2, g.isSwap, g.schmidtRank);
1732 }
1733 else if (g.numQubits == 3)
1734 {
1735 if (!g.isSwap) // CCX
1736 {
1737 ApplyTwoQubitFastGate(g.q3, g.q2, false, 2);
1738 ApplyTwoQubitFastGate(g.q2, g.q1, false, 2);
1739 ApplyTwoQubitFastGate(g.q3, g.q2, false, 2);
1740 ApplyTwoQubitFastGate(g.q2, g.q1, false, 2);
1741 ApplyTwoQubitFastGate(g.q3, g.q1, false, 2);
1742 }
1743 else // CSwap
1744 {
1745 ApplyTwoQubitFastGate(g.q2, g.q3, false, 2);
1746 ApplyTwoQubitFastGate(g.q3, g.q2, false, 2);
1747 ApplyTwoQubitFastGate(g.q2, g.q1, false, 2);
1748 ApplyTwoQubitFastGate(g.q3, g.q2, false, 2);
1749 ApplyTwoQubitFastGate(g.q2, g.q1, false, 2);
1750 ApplyTwoQubitFastGate(g.q3, g.q1, false, 2);
1751 ApplyTwoQubitFastGate(g.q2, g.q3, false, 2);
1752 }
1753 }
1754 }
1755
1756 bool IsRelabelingSwap(const FastGateOp &gate) const
1757 {
1758 return swapGatesRelabelQubits && gate.numQubits == 2 && gate.isSwap;
1759 }
1760
1761 struct PartnerPull
1762 {
1763 IndexType pos = 0;
1764 double weight = 0.0;
1765 };
1766
1767 // Scan upcoming pre-parsed gates to collect weighted physical positions of
1768 // the next partners of qubit1 and qubit2.
1769 size_t CollectNextPartnerPullsFast(IndexType qubit1, IndexType qubit2, const FastGateOp *window, size_t windowSize, size_t stepIdx,
1770 std::array<PartnerPull, 16> &pulls, double &totalWeight) const
1771 {
1772 size_t numPulls = 0;
1773 totalWeight = 0.0;
1774 bool foundQ1 = false;
1775 bool foundQ2 = false;
1776 double weight = 1.0;
1777 const size_t maxStep = std::min(windowSize, stepIdx + 9);
1778 std::array<std::pair<IndexType, IndexType>, 8> relabels;
1779 size_t numRelabels = 0;
1780 auto originalLabel = [&](IndexType q) {
1781 for (size_t i = numRelabels; i > 0; --i)
1782 {
1783 const auto [a, b] = relabels[i - 1];
1784 if (q == a)
1785 q = b;
1786 else if (q == b)
1787 q = a;
1788 }
1789 return q;
1790 };
1791
1792 for (size_t s = stepIdx + 1; s < maxStep; ++s)
1793 {
1794 const FastGateOp &fg = window[s];
1795 if (IsRelabelingSwap(fg))
1796 {
1797 relabels[numRelabels++] = {fg.q1, fg.q2};
1798 continue;
1799 }
1800 const IndexType qA = originalLabel(fg.q1);
1801 const IndexType qB = originalLabel(fg.q2);
1802
1803 auto recordPartner = [&](IndexType activeQ, IndexType partnerQ) {
1804 if (partnerQ == qubit1 || partnerQ == qubit2)
1805 return;
1806 const bool isFirst = (activeQ == qubit1) ? !foundQ1 : !foundQ2;
1807 const double w = isFirst ? weight : (weight * 0.35);
1808 if (numPulls < pulls.size())
1809 {
1810 pulls[numPulls++] = {qubitsMap[partnerQ], w};
1811 totalWeight += w;
1812 }
1813 if (activeQ == qubit1)
1814 foundQ1 = true;
1815 else
1816 foundQ2 = true;
1817 };
1818
1819 if (qA == qubit1 || qA == qubit2)
1820 recordPartner(qA, qB);
1821 if (qB == qubit1 || qB == qubit2)
1822 recordPartner(qB, qA);
1823
1824 if (foundQ1 && foundQ2)
1825 break;
1826 weight *= 0.6;
1827 }
1828
1829 return numPulls;
1830 }
1831
1832 IndexType ComputeNextPartnerGravityMeetPositionFast(IndexType realq1, IndexType realq2, IndexType qubit1, IndexType qubit2, const FastGateOp *window,
1833 size_t windowSize, size_t stepIdx) const
1834 {
1835 assert(realq1 < realq2);
1836
1837 std::array<PartnerPull, 16> pulls;
1838 double totalWeight = 0.0;
1839 const size_t numPulls = CollectNextPartnerPullsFast(qubit1, qubit2, window, windowSize, stepIdx, pulls, totalWeight);
1840 if (numPulls == 0 || totalWeight <= 0.0)
1841 return ComputeAnchorHeavierMeetPosition(realq1, realq2);
1842
1843 IndexType bestPos = realq1;
1844 double bestDist = std::numeric_limits<double>::infinity();
1845 double bestLocalBondCost = std::numeric_limits<double>::infinity();
1846
1847 for (IndexType m = realq1; m < realq2; ++m)
1848 {
1849 double dist = 0.0;
1850 for (size_t k = 0; k < numPulls; ++k)
1851 dist += pulls[k].weight * static_cast<double>(std::abs(m - pulls[k].pos));
1852
1853 const double localCost = bondCost[m];
1854 if (dist < bestDist - 1e-9 || (std::abs(dist - bestDist) <= 1e-9 && localCost < bestLocalBondCost))
1855 {
1856 bestDist = dist;
1857 bestLocalBondCost = localCost;
1858 bestPos = m;
1859 }
1860 }
1861
1862 return bestPos;
1863 }
1864
1865 // Compute the meeting position that minimizes the exact post-step bond cost
1866 // in O(realq2 - realq1) time, optionally tilted by L1 partner distance.
1867 IndexType ComputeMinOneStepCostMeetPositionFast(IndexType realq1, IndexType realq2, IndexType qubit1, IndexType qubit2, const FastGateOp *window,
1868 size_t windowSize, size_t stepIdx, bool includeGravity) const
1869 {
1870 assert(realq1 < realq2);
1871 const IndexType span = realq2 - realq1;
1872 if (span <= 1)
1873 return realq1;
1874
1875 const FastGateOp &fg = window[stepIdx];
1876 const bool isSwap = (fg.numQubits == 2) ? fg.isSwap : false;
1877 const double schmidtRank = fg.schmidtRank;
1878
1879 constexpr IndexType kMaxStackSpan = 128;
1880 std::array<double, kMaxStackSpan> rightSweepDimStack{};
1881 std::array<double, kMaxStackSpan> prefixPostCostStack{};
1882 std::array<double, kMaxStackSpan> leftSweepDimStack{};
1883 std::array<double, kMaxStackSpan> suffixPostCostStack{};
1884
1885 std::vector<double> heapBuf;
1886 double *rightSweepDim = rightSweepDimStack.data();
1887 double *prefixPostCost = prefixPostCostStack.data();
1888 double *leftSweepDim = leftSweepDimStack.data();
1889 double *suffixPostCost = suffixPostCostStack.data();
1890
1891 if (span > kMaxStackSpan)
1892 {
1893 heapBuf.resize(static_cast<size_t>(span) * 4, 0.0);
1894 rightSweepDim = heapBuf.data();
1895 prefixPostCost = rightSweepDim + span;
1896 leftSweepDim = prefixPostCost + span;
1897 suffixPostCost = leftSweepDim + span;
1898 }
1899
1900 prefixPostCost[0] = 0.0;
1901 for (IndexType i = 0; i < span - 1; ++i)
1902 {
1903 const IndexType bond = realq1 + i;
1904 const double leftD = (i == 0) ? ((realq1 > 0) ? currentBondDim[realq1 - 1] : 1.0) : rightSweepDim[i - 1];
1905 const double betweenD = currentBondDim[bond];
1906 const double rightD = currentBondDim[bond + 1];
1907 const double newD = EstimateGrownBondDimension(bond, leftD, betweenD, rightD, true, 4);
1908 rightSweepDim[i] = newD;
1909 prefixPostCost[i + 1] = prefixPostCost[i] + BondCost(newD);
1910 }
1911
1912 suffixPostCost[span - 1] = 0.0;
1913 for (IndexType i = span - 1; i > 0; --i)
1914 {
1915 const IndexType bond = realq1 + i;
1916 const double leftD = currentBondDim[bond - 1];
1917 const double betweenD = currentBondDim[bond];
1918 const double rightD =
1919 (i == span - 1) ? ((realq2 < static_cast<IndexType>(currentBondDim.size())) ? currentBondDim[realq2] : 1.0) : leftSweepDim[i + 1];
1920 const double newD = EstimateGrownBondDimension(bond, leftD, betweenD, rightD, true, 4);
1921 leftSweepDim[i] = newD;
1922 suffixPostCost[i - 1] = suffixPostCost[i] + BondCost(newD);
1923 }
1924
1925 std::array<PartnerPull, 16> pulls;
1926 double totalPullWeight = 0.0;
1927 const size_t numPulls = CollectNextPartnerPullsFast(qubit1, qubit2, window, windowSize, stepIdx, pulls, totalPullWeight);
1928 const double gravityWeight = includeGravity ? 0.15 : 1e-6;
1929 const double invNorm = (numPulls > 0 && totalPullWeight > 0.0) ? (gravityWeight / (static_cast<double>(span) * totalPullWeight)) : 0.0;
1930
1931 IndexType bestPos = realq1;
1932 double bestScore = std::numeric_limits<double>::infinity();
1933
1934 for (IndexType i = 0; i < span; ++i)
1935 {
1936 const IndexType m = realq1 + i;
1937 const double leftD = (i == 0) ? ((realq1 > 0) ? currentBondDim[realq1 - 1] : 1.0) : rightSweepDim[i - 1];
1938 const double rightD =
1939 (i == span - 1) ? ((realq2 < static_cast<IndexType>(currentBondDim.size())) ? currentBondDim[realq2] : 1.0) : leftSweepDim[i + 1];
1940 const double gateD = EstimateGrownBondDimension(m, leftD, currentBondDim[m], rightD, isSwap, schmidtRank);
1941 const double gateCost = BondCost(gateD);
1942 // Share the exact post-growth cost coefficients (kPostSwapCostWeight * S +
1943 // kPostGateCostWeight * G) with SwapQubitsToPosition and ApplyGate so the
1944 // one-step objective matches the rollout's cost model exactly.
1945 const double postBondCost = kPostSwapCostWeight * (prefixPostCost[i] + suffixPostCost[i]) + kPostGateCostWeight * gateCost;
1946
1947 double score = postBondCost;
1948 if (invNorm > 0.0)
1949 {
1950 double pullDist = 0.0;
1951 for (size_t k = 0; k < numPulls; ++k)
1952 pullDist += pulls[k].weight * static_cast<double>(std::abs(m - pulls[k].pos));
1953 score *= (1.0 + invNorm * pullDist);
1954 }
1955
1956 if (score < bestScore)
1957 {
1958 bestScore = score;
1959 bestPos = m;
1960 }
1961 }
1962
1963 return bestPos;
1964 }
1965
1966 IndexType SelectMeetPositionForPolicyFast(HeuristicPolicy policy, IndexType realq1, IndexType realq2, IndexType qubit1, IndexType qubit2,
1967 const FastGateOp *window, size_t windowSize, size_t stepIdx) const
1968 {
1969 if (realq2 - realq1 <= 1)
1970 return realq1;
1971
1972 switch (policy)
1973 {
1975 return ComputeMinOneStepCostMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx, true);
1977 return ComputeMinOneStepCostMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx, false);
1979 return ComputeNextPartnerGravityMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx);
1981 return ComputeAnchorHeavierMeetPosition(realq1, realq2);
1983 return ComputeHeuristicMeetPosition(realq1, realq2);
1985 return realq1;
1987 return realq2 - 1;
1989 return (realq1 <= static_cast<IndexType>(nrQubits) - 1 - realq2) ? realq1 : (realq2 - 1);
1991 const IndexType center = std::max<IndexType>(0, (static_cast<IndexType>(nrQubits) - 2) / 2);
1992 return std::clamp(center, realq1, realq2 - 1);
1993 }
1994 }
1995
1996 return ComputeHeuristicMeetPosition(realq1, realq2);
1997 }
1998
1999 // Evaluate a portfolio of heuristic rollout policies over the remaining
2000 // lookaheadDepth + 1 two-qubit gates using pre-parsed FastGateOp entries and
2001 // zero-allocation stack snapshots.
2002 IndexType EvaluateHeuristicRolloutsFast(const FastGateOp *window, size_t windowSize, size_t stepIdx, int lookaheadDepth, double currentCost,
2003 double &bestCost)
2004 {
2005 if (stepIdx >= windowSize)
2006 {
2007 if (currentCost < bestCost)
2008 bestCost = currentCost;
2009 return 0;
2010 }
2011
2012 const FastGateOp &firstOp = window[stepIdx];
2013 const IndexType firstQ1 = firstOp.q1;
2014 const IndexType firstQ2 = firstOp.q2;
2015 IndexType firstReal1 = qubitsMap[firstQ1];
2016 IndexType firstReal2 = qubitsMap[firstQ2];
2017 if (firstReal1 > firstReal2)
2018 std::swap(firstReal1, firstReal2);
2019
2020 const HeuristicPolicy defaultFirstPolicy = (lookaheadDepth <= 0) ? HeuristicPolicy::kMinOneStepCost : HeuristicPolicy::kCombinedCostAndGravity;
2021 IndexType bestFirstPosition = IsRelabelingSwap(firstOp) ? -1
2022 : (firstReal2 - firstReal1 > 1) ? SelectMeetPositionForPolicyFast(defaultFirstPolicy, firstReal1, firstReal2, firstQ1,
2023 firstQ2, window, windowSize, stepIdx)
2024 : firstReal1;
2025
2026 static constexpr std::array<HeuristicPolicy, 9> kFullPortfolio = {
2036 };
2037
2038 const size_t numPolicies = (lookaheadDepth <= 0) ? 1 : kFullPortfolio.size();
2039 const size_t endStep = std::min(windowSize, stepIdx + static_cast<size_t>(std::max(0, lookaheadDepth)) + 1);
2040
2041 FastStateSnapshot baseState;
2042 baseState.SaveFrom(*this);
2043
2044 for (size_t pi = 0; pi < numPolicies; ++pi)
2045 {
2046 const HeuristicPolicy policy = (lookaheadDepth <= 0) ? HeuristicPolicy::kMinOneStepCost : kFullPortfolio[pi];
2047 if (pi > 0)
2048 baseState.RestoreTo(*this);
2050
2051 double rolloutCost = currentCost;
2052 IndexType chosenFirstPos = bestFirstPosition;
2053 bool pruned = false;
2054
2055 for (size_t s = stepIdx; s < endStep; ++s)
2056 {
2057 const FastGateOp &fg = window[s];
2058 const IndexType q1 = fg.q1;
2059 const IndexType q2 = fg.q2;
2060 IndexType rq1 = qubitsMap[q1];
2061 IndexType rq2 = qubitsMap[q2];
2062 if (rq1 > rq2)
2063 std::swap(rq1, rq2);
2064
2065 IndexType meetPos = IsRelabelingSwap(fg) ? -1 : rq1;
2066 if (!IsRelabelingSwap(fg) && rq2 - rq1 > 1)
2067 {
2068 meetPos = SelectMeetPositionForPolicyFast(policy, rq1, rq2, q1, q2, window, windowSize, s);
2069 SwapQubitsToPosition(q1, q2, meetPos);
2070 }
2071
2072 if (s == stepIdx)
2073 chosenFirstPos = meetPos;
2074
2075 ApplyFastGate(fg);
2076 rolloutCost = currentCost + getTotalSwappingCost();
2077
2078 if (rolloutCost >= bestCost)
2079 {
2080 pruned = true;
2081 break;
2082 }
2083 }
2084
2085 if (!pruned && rolloutCost < bestCost)
2086 {
2087 bestCost = rolloutCost;
2088 bestFirstPosition = chosenFirstPos;
2089 }
2090 }
2091
2092 baseState.RestoreTo(*this);
2093 return bestFirstPosition;
2094 }
2095
2096 void EvaluateMeetingPositionCostFast(IndexType meetPosition, const FastGateOp *window, size_t windowSize, size_t stepIdx, int lookaheadDepth,
2097 int lookaheadDepthWithHeuristic, double currentCost, double &bestCost, bool mutateSelf)
2098 {
2099 if (stepIdx >= windowSize)
2100 {
2101 if (currentCost < bestCost)
2102 bestCost = currentCost;
2103 return;
2104 }
2105
2106 FastStateSnapshot saved;
2107 if (!mutateSelf)
2108 saved.SaveFrom(*this);
2109
2110 const double ccost = getTotalSwappingCost();
2111 const FastGateOp &fg = window[stepIdx];
2112 IndexType realq1 = qubitsMap[fg.q1];
2113 IndexType realq2 = qubitsMap[fg.q2];
2114 if (realq1 > realq2)
2115 std::swap(realq1, realq2);
2116
2117 if (!IsRelabelingSwap(fg) && realq2 - realq1 > 1)
2118 SwapQubitsToPosition(fg.q1, fg.q2, meetPosition);
2119
2120 ApplyFastGate(fg);
2121 currentCost += (getTotalSwappingCost() - ccost);
2122
2123 if (currentCost < bestCost)
2124 {
2125 if (lookaheadDepth <= 0 || stepIdx + 1 >= windowSize)
2126 {
2127 bestCost = currentCost;
2128 }
2129 else
2130 {
2131 FindBestMeetingPositionFast(window, windowSize, stepIdx + 1, lookaheadDepth - 1, lookaheadDepthWithHeuristic, currentCost, bestCost);
2132 }
2133 }
2134
2135 if (!mutateSelf)
2136 saved.RestoreTo(*this);
2137 }
2138
2139 IndexType FindBestMeetingPositionFast(const FastGateOp *window, size_t windowSize, size_t stepIdx, int lookaheadDepth, int lookaheadDepthWithHeuristic,
2140 double currentCost, double &bestCost)
2141 {
2142 if (stepIdx >= windowSize)
2143 {
2144 if (currentCost < bestCost)
2145 bestCost = currentCost;
2146 return 0;
2147 }
2148
2149 const FastGateOp &fg = window[stepIdx];
2150 const IndexType qubit1 = fg.q1;
2151 const IndexType qubit2 = fg.q2;
2152
2153 if (IsRelabelingSwap(fg))
2154 {
2155 EvaluateMeetingPositionCostFast(-1, window, windowSize, stepIdx, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost, false);
2156 return -1; // A logical SWAP needs no meeting position or branching.
2157 }
2158
2159 IndexType realq1 = qubitsMap[qubit1];
2160 IndexType realq2 = qubitsMap[qubit2];
2161 if (realq1 > realq2)
2162 std::swap(realq1, realq2);
2163
2164 if (lookaheadDepth <= 0 || lookaheadDepth <= lookaheadDepthWithHeuristic)
2165 {
2166 return EvaluateHeuristicRolloutsFast(window, windowSize, stepIdx, std::max(0, lookaheadDepth), currentCost, bestCost);
2167 }
2168
2169 if (realq2 - realq1 <= 1)
2170 {
2171 EvaluateMeetingPositionCostFast(realq1, window, windowSize, stepIdx, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost, false);
2172 return realq1;
2173 }
2174
2175 // At the root (stepIdx == 0), first evaluate the full 9-policy rollout
2176 // portfolio to establish a tight upper bound in bestCost for pruning.
2177 IndexType bestPosition = realq1;
2178 if (stepIdx == 0)
2179 {
2180 bestPosition = EvaluateHeuristicRolloutsFast(window, windowSize, stepIdx, lookaheadDepth, currentCost, bestCost);
2181 }
2182
2183 const size_t span = static_cast<size_t>(realq2 - realq1);
2184 std::array<IndexType, 128> stackCandidates;
2185 std::vector<IndexType> heapCandidates;
2186 IndexType *candidates = stackCandidates.data();
2187 size_t maxCandidates = stackCandidates.size();
2188 if (span > stackCandidates.size())
2189 {
2190 heapCandidates.resize(span);
2191 candidates = heapCandidates.data();
2192 maxCandidates = span;
2193 }
2194
2195 size_t numCandidates = 0;
2196 auto addCandidate = [&](IndexType pos) {
2197 if (pos < realq1 || pos >= realq2)
2198 return;
2199 for (size_t i = 0; i < numCandidates; ++i)
2200 if (candidates[i] == pos)
2201 return;
2202 if (numCandidates < maxCandidates)
2203 candidates[numCandidates++] = pos;
2204 };
2205
2206 if (stepIdx == 0)
2207 addCandidate(bestPosition);
2208 addCandidate(ComputeMinOneStepCostMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx, true));
2209 addCandidate(ComputeMinOneStepCostMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx, false));
2210 addCandidate(ComputeHeuristicMeetPosition(realq1, realq2));
2211 addCandidate(ComputeAnchorHeavierMeetPosition(realq1, realq2));
2212 addCandidate(ComputeNextPartnerGravityMeetPositionFast(realq1, realq2, qubit1, qubit2, window, windowSize, stepIdx));
2213 addCandidate(realq1);
2214 addCandidate(realq2 - 1);
2215 const IndexType centerPos = std::clamp(std::max<IndexType>(0, (static_cast<IndexType>(nrQubits) - 2) / 2), realq1, realq2 - 1);
2216 addCandidate(centerPos);
2217
2218 // Enumerate all remaining positions in [realq1, realq2 - 1] after the
2219 // priority heuristic candidates so branch-and-bound prunes early.
2220 for (IndexType m = realq1; m < realq2; ++m)
2221 addCandidate(m);
2222
2223 if (stepIdx > 0)
2224 bestPosition = candidates[0];
2225
2226 for (size_t i = 0; i < numCandidates; ++i)
2227 {
2228 const IndexType m = candidates[i];
2229 const double oldCost = bestCost;
2230 EvaluateMeetingPositionCostFast(m, window, windowSize, stepIdx, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost, false);
2231
2232 if (bestCost < oldCost)
2233 bestPosition = m;
2234 }
2235
2236 return bestPosition;
2237 }
2238
2239 // P0: Cut-preserving meeting position selector with O(span) prefix/suffix
2240 // future displacement tables for both active endpoints and all shifted
2241 // intermediate qubits in (realq1, realq2).
2242 IndexType SelectMeetingPositionP0Fast(IndexType realq1, IndexType realq2, IndexType qubit1, IndexType qubit2, const FastGateOp *window, size_t windowSize,
2243 size_t stepIdx, bool includeGravity) const
2244 {
2245 assert(realq1 < realq2);
2246 const IndexType span = realq2 - realq1;
2247 if (span <= 1)
2248 return realq1;
2249
2250 const IndexType nBonds = static_cast<IndexType>(currentBondDim.size());
2251 constexpr IndexType kMaxStackSpan = 128;
2252 std::array<double, kMaxStackSpan + 2> bondsStack{};
2253 std::array<double, kMaxStackSpan> prefixCutStack{};
2254 std::array<double, kMaxStackSpan> suffixCutStack{};
2255 std::array<double, kMaxStackSpan> interShiftStack{};
2256 std::vector<double> heapBuf;
2257
2258 double *bonds = bondsStack.data();
2259 double *prefixCut = prefixCutStack.data();
2260 double *suffixCut = suffixCutStack.data();
2261 double *interShift = interShiftStack.data();
2262 if (span > kMaxStackSpan)
2263 {
2264 heapBuf.resize(static_cast<size_t>(span) * 4 + 2, 0.0);
2265 bonds = heapBuf.data();
2266 prefixCut = bonds + span + 2;
2267 suffixCut = prefixCut + span;
2268 interShift = suffixCut + span;
2269 }
2270
2271 bonds[0] = (realq1 > 0) ? currentBondDim[realq1 - 1] : 1.0;
2272 for (IndexType i = 0; i < span; ++i)
2273 bonds[i + 1] = currentBondDim[realq1 + i];
2274 bonds[span + 1] = (realq2 < nBonds) ? currentBondDim[realq2] : 1.0;
2275
2276 const double E1 = std::max(bonds[0], bonds[1]);
2277 const double E2 = std::max(bonds[span], bonds[span + 1]);
2278 const double d = static_cast<double>(physExtent);
2279
2280 prefixCut[0] = 0.0;
2281 for (IndexType i = 0; i < span - 1; ++i)
2282 {
2283 const double bDim = bonds[i + 1];
2284 double dmg = 0.0;
2285 if (E1 > bDim)
2286 {
2287 const double inflated = std::min(E1, d * bDim);
2288 dmg = std::max(0.0, BondCost(inflated) - bondCost[realq1 + i]);
2289 }
2290 prefixCut[i + 1] = prefixCut[i] + dmg;
2291 }
2292
2293 suffixCut[span - 1] = 0.0;
2294 for (IndexType i = span - 1; i > 0; --i)
2295 {
2296 const double bDim = bonds[i + 1];
2297 double dmg = 0.0;
2298 if (E2 > bDim)
2299 {
2300 const double inflated = std::min(E2, d * bDim);
2301 dmg = std::max(0.0, BondCost(inflated) - bondCost[realq1 + i]);
2302 }
2303 suffixCut[i - 1] = suffixCut[i] + dmg;
2304 }
2305
2306 std::array<PartnerPull, 16> pulls;
2307 double totalPullWeight = 0.0;
2308 size_t numPulls = 0;
2309
2310 if (includeGravity && window && stepIdx + 1 < windowSize)
2311 {
2312 numPulls = CollectNextPartnerPullsFast(qubit1, qubit2, window, windowSize, stepIdx, pulls, totalPullWeight);
2313
2314 // Compute O(span) prefix/suffix future distance change for all
2315 // intermediate qubits shifted by meeting at m = realq1 + i:
2316 // - sites k in (realq1, m] shift left by -1
2317 // - sites k in [m + 1, realq2) shift right by +1
2318 std::array<IndexType, kMaxStackSpan> nextPartnerPosStack;
2319 std::array<double, kMaxStackSpan> nextPartnerWStack{};
2320 std::vector<IndexType> nextPartnerPosHeap;
2321 std::vector<double> nextPartnerWHeap;
2322 IndexType *nextPartnerPos = nextPartnerPosStack.data();
2323 double *nextPartnerW = nextPartnerWStack.data();
2324 if (span > kMaxStackSpan)
2325 {
2326 nextPartnerPosHeap.assign(static_cast<size_t>(span), -1);
2327 nextPartnerWHeap.assign(static_cast<size_t>(span), 0.0);
2328 nextPartnerPos = nextPartnerPosHeap.data();
2329 nextPartnerW = nextPartnerWHeap.data();
2330 }
2331 else
2332 {
2333 for (IndexType i = 0; i < span; ++i)
2334 {
2335 nextPartnerPos[i] = -1;
2336 nextPartnerW[i] = 0.0;
2337 }
2338 }
2339
2340 const size_t maxLook = std::min(windowSize, stepIdx + 13);
2341 double wDecay = 1.0;
2342 int foundCount = 0;
2343 std::array<std::pair<IndexType, IndexType>, 12> relabels;
2344 size_t numRelabels = 0;
2345 auto originalLabel = [&](IndexType q) {
2346 for (size_t i = numRelabels; i > 0; --i)
2347 {
2348 const auto [a, b] = relabels[i - 1];
2349 if (q == a)
2350 q = b;
2351 else if (q == b)
2352 q = a;
2353 }
2354 return q;
2355 };
2356
2357 for (size_t s = stepIdx + 1; s < maxLook && foundCount < span - 1; ++s)
2358 {
2359 const FastGateOp &fg = window[s];
2360 if (IsRelabelingSwap(fg))
2361 {
2362 if (numRelabels < relabels.size())
2363 relabels[numRelabels++] = {fg.q1, fg.q2};
2364 continue;
2365 }
2366 const IndexType qA = originalLabel(fg.q1);
2367 const IndexType qB = originalLabel(fg.q2);
2368 const IndexType rA = qubitsMap[qA];
2369 const IndexType rB = qubitsMap[qB];
2370 if (rA > realq1 && rA < realq2 && nextPartnerPos[rA - realq1] < 0)
2371 {
2372 nextPartnerPos[rA - realq1] = rB;
2373 nextPartnerW[rA - realq1] = wDecay;
2374 ++foundCount;
2375 }
2376 if (rB > realq1 && rB < realq2 && nextPartnerPos[rB - realq1] < 0)
2377 {
2378 nextPartnerPos[rB - realq1] = rA;
2379 nextPartnerW[rB - realq1] = wDecay;
2380 ++foundCount;
2381 }
2382 wDecay *= 0.82;
2383 }
2384
2385 double runningLeftShift = 0.0;
2386 interShift[0] = 0.0;
2387 for (IndexType i = 1; i < span; ++i)
2388 {
2389 const IndexType p = nextPartnerPos[i];
2390 if (p >= 0)
2391 {
2392 const IndexType kPos = realq1 + i;
2393 const double delta = (p < kPos) ? -1.0 : ((p > kPos) ? +1.0 : 0.0);
2394 runningLeftShift += nextPartnerW[i] * delta;
2395 }
2396 interShift[i] = runningLeftShift;
2397 }
2398
2399 double runningRightShift = 0.0;
2400 for (IndexType i = span - 2; i >= 0; --i)
2401 {
2402 const IndexType p = nextPartnerPos[i + 1];
2403 if (p >= 0)
2404 {
2405 const IndexType kPos = realq1 + i + 1;
2406 const double delta = (p > kPos) ? -1.0 : ((p < kPos) ? +1.0 : 0.0);
2407 runningRightShift += nextPartnerW[i + 1] * delta;
2408 }
2409 interShift[i] += runningRightShift;
2410 }
2411 }
2412
2413 const double c1 = bonds[0] * bonds[1];
2414 const double c2 = bonds[span] * bonds[span + 1];
2415 const double effCap = (runtimeBondCap > 0) ? static_cast<double>(runtimeBondCap) : std::numeric_limits<double>::infinity();
2416
2417 IndexType bestPos = realq1;
2418 double bestScore = std::numeric_limits<double>::infinity();
2419
2420 for (IndexType i = 0; i < span; ++i)
2421 {
2422 const IndexType m = realq1 + i;
2423 const double bLeft = std::min(bonds[i], effCap);
2424 const double bMid = std::min(bonds[i + 1], effCap);
2425 const double bRight = std::min(bonds[i + 2], effCap);
2426 const double localBondCost = bondCost[m];
2427 const double cutDamage = 0.5 * (prefixCut[i] + suffixCut[i]);
2428 const double outerContext = 0.05 * (bLeft * bMid * bRight);
2429 const double tieBreak = (c2 > c1) ? (1e-6 * static_cast<double>(span - 1 - i)) : (1e-6 * static_cast<double>(i));
2430 const double baseCost = localBondCost + cutDamage + outerContext + tieBreak;
2431
2432 double pullCost = 0.0;
2433 if (includeGravity)
2434 {
2435 for (size_t k = 0; k < numPulls; ++k)
2436 pullCost += pulls[k].weight * static_cast<double>(std::abs(m - pulls[k].pos));
2437 pullCost += 1.15 * interShift[i];
2438 }
2439
2440 const double score = baseCost + 0.35 * localBondCost * (pullCost / static_cast<double>(span));
2441 if (score < bestScore)
2442 {
2443 bestScore = score;
2444 bestPos = m;
2445 }
2446 }
2447
2448 return bestPos;
2449 }
2450
2451 public:
2452 // 7b: Adaptive meeting-position planner with dynamic multi-step pruned rollout
2453 // activation, common gate horizon, and configurable branching depth.
2454 IndexType SelectMeetingPositionAdaptive(const std::vector<std::shared_ptr<Circuits::IOperation<>>> &upcomingGates, long long int currentGateIndex,
2455 int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost)
2456 {
2457 if (currentGateIndex < 0 || currentGateIndex >= static_cast<long long int>(upcomingGates.size()))
2458 {
2459 if (currentCost < bestCost)
2460 bestCost = currentCost;
2461 return -1;
2462 }
2463
2464 const int exactSteps = std::max(0, lookaheadDepth - std::max(0, lookaheadDepthWithHeuristic));
2465
2466 // Delegate zero-heuristic exhaustive search (lookaheadDepthWithHeuristic <= 0)
2467 // or depth-0 queries to FindBestMeetingPosition.
2468 if (lookaheadDepth <= 0 || lookaheadDepthWithHeuristic <= 0)
2469 return FindBestMeetingPosition(upcomingGates, currentGateIndex, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost);
2470
2471 const size_t maxNeeded = static_cast<size_t>(std::max(16, 2 * std::max(0, lookaheadDepth) + 10));
2472 std::array<FastGateOp, 128> stackWindow;
2473 std::vector<FastGateOp> heapWindow;
2474 FastGateOp *windowPtr = stackWindow.data();
2475 if (maxNeeded > stackWindow.size())
2476 {
2477 heapWindow.resize(maxNeeded);
2478 windowPtr = heapWindow.data();
2479 }
2480 const size_t windowSize = ExtractFastGateWindow(upcomingGates, currentGateIndex, maxNeeded, windowPtr);
2481 if (windowSize == 0)
2482 {
2483 if (currentCost < bestCost)
2484 bestCost = currentCost;
2485 return -1;
2486 }
2487
2488 // Common gate horizon shared by all candidates and rollout policies so
2489 // routes that make future gates adjacent are never penalized by running more gates.
2490 const size_t endStep = std::min(windowSize, static_cast<size_t>(std::max(0, lookaheadDepth)) + 1);
2491
2492 const FastGateOp &fg0 = windowPtr[0];
2493 if (IsRelabelingSwap(fg0))
2494 {
2495 EvaluateMeetingPositionCostFast(-1, windowPtr, windowSize, 0, lookaheadDepth, lookaheadDepthWithHeuristic, currentCost, bestCost, false);
2496 return -1;
2497 }
2498
2499 auto searchAdaptive = [&](auto &self, size_t stepIdx, double stepStartCost, double &boundCost) -> IndexType {
2500 if (stepIdx >= endStep)
2501 {
2502 if (stepStartCost < boundCost)
2503 boundCost = stepStartCost;
2504 return -1;
2505 }
2506
2507 const FastGateOp &fg = windowPtr[stepIdx];
2508 IndexType rq1 = -1;
2509 IndexType rq2 = -1;
2510 if (!IsRelabelingSwap(fg))
2511 {
2512 rq1 = qubitsMap[fg.q1];
2513 rq2 = qubitsMap[fg.q2];
2514 if (rq1 > rq2)
2515 std::swap(rq1, rq2);
2516 }
2517
2518 FastStateSnapshot stepState;
2519 stepState.SaveFrom(*this);
2520
2521 // Always evaluate the full heuristic rollout portfolio from stepIdx over
2522 // [stepIdx, endStep) first. This serves as the leaf evaluation when
2523 // stepIdx >= exactSteps and preserves the heuristic portfolio's best result
2524 // as a tight upper-bound baseline before any branching at stepIdx < exactSteps.
2525 // The full portfolio matters: positional policies (swap left/right, closer
2526 // to an end or the center) win different decisions than the cost-based
2527 // ones, and a three-policy subset lost exactness on large QFTs.
2528 IndexType bestPos = IsRelabelingSwap(fg) ? -1
2529 : (rq2 - rq1 <= 1) ? rq1
2530 : SelectMeetingPositionP0Fast(rq1, rq2, fg.q1, fg.q2, windowPtr, windowSize, stepIdx, true);
2531
2532 const double boundBefore = boundCost;
2533 const IndexType rolloutPos =
2534 EvaluateHeuristicRolloutsFast(windowPtr, windowSize, stepIdx, static_cast<int>(endStep - stepIdx) - 1, stepStartCost, boundCost);
2535 if (boundCost < boundBefore)
2536 bestPos = rolloutPos;
2537
2538 stepState.RestoreTo(*this);
2539 if (stepIdx >= static_cast<size_t>(exactSteps))
2540 return bestPos;
2541
2542 if (IsRelabelingSwap(fg))
2543 {
2544 ApplyFastGate(fg);
2545 if (stepStartCost < boundCost)
2546 {
2547 if (stepIdx + 1 >= endStep)
2548 boundCost = stepStartCost;
2549 else
2550 (void)self(self, stepIdx + 1, stepStartCost, boundCost);
2551 }
2552 stepState.RestoreTo(*this);
2553 return -1;
2554 }
2555
2556 if (rq2 - rq1 <= 1)
2557 {
2559 ApplyFastGate(fg);
2560 const double nextCost = stepStartCost + getTotalSwappingCost();
2561 if (nextCost < boundCost)
2562 {
2563 if (stepIdx + 1 >= endStep)
2564 boundCost = nextCost;
2565 else
2566 (void)self(self, stepIdx + 1, nextCost, boundCost);
2567 }
2568 stepState.RestoreTo(*this);
2569 return rq1;
2570 }
2571
2572 const IndexType mP0Grav = SelectMeetingPositionP0Fast(rq1, rq2, fg.q1, fg.q2, windowPtr, windowSize, stepIdx, true);
2573 const IndexType mP0Pure = SelectMeetingPositionP0Fast(rq1, rq2, fg.q1, fg.q2, windowPtr, windowSize, stepIdx, false);
2574 const IndexType mAnchor = ComputeAnchorHeavierMeetPosition(rq1, rq2);
2575
2576 double minBondInSpan = currentBondDim[rq1];
2577 for (IndexType b = rq1 + 1; b < rq2; ++b)
2578 minBondInSpan = std::min(minBondInSpan, currentBondDim[b]);
2579
2580 const double E1 = std::max((rq1 > 0) ? currentBondDim[rq1 - 1] : 1.0, currentBondDim[rq1]);
2581 const double E2 = std::max(currentBondDim[rq2 - 1], (rq2 < static_cast<IndexType>(currentBondDim.size())) ? currentBondDim[rq2] : 1.0);
2582 const bool hasLowEntanglementCut = (minBondInSpan < 16.0) && (runtimeBondCap <= 0 || minBondInSpan < static_cast<double>(runtimeBondCap)) &&
2583 (minBondInSpan <= 0.5 * std::min(E1, E2));
2584
2585 const IndexType span = rq2 - rq1;
2586 std::array<IndexType, 128> stackCandidates{};
2587 std::vector<IndexType> heapCandidates;
2588 IndexType *candidates = stackCandidates.data();
2589 size_t maxCandidates = stackCandidates.size();
2590 if (static_cast<size_t>(span) > stackCandidates.size())
2591 {
2592 heapCandidates.resize(static_cast<size_t>(span));
2593 candidates = heapCandidates.data();
2594 maxCandidates = heapCandidates.size();
2595 }
2596
2597 size_t numCandidates = 0;
2598 auto addCand = [&](IndexType pos) {
2599 if (pos < rq1 || pos >= rq2)
2600 return;
2601 for (size_t i = 0; i < numCandidates; ++i)
2602 if (candidates[i] == pos)
2603 return;
2604 if (numCandidates < maxCandidates)
2605 candidates[numCandidates++] = pos;
2606 };
2607
2608 addCand(bestPos);
2609 addCand(mP0Grav);
2610 if (!hasLowEntanglementCut)
2611 {
2612 addCand(mP0Pure);
2613 addCand(mAnchor);
2614 addCand(rq1);
2615 addCand(rq2 - 1);
2616 addCand(rq1 + span / 2);
2617 if (span <= 12 || stepIdx > 0 || exactSteps >= 2)
2618 {
2619 for (IndexType m = rq1; m < rq2; ++m)
2620 addCand(m);
2621 }
2622 }
2623
2624 for (size_t ci = 0; ci < numCandidates; ++ci)
2625 {
2626 const IndexType candPos = candidates[ci];
2627 stepState.RestoreTo(*this);
2629
2630 SwapQubitsToPosition(fg.q1, fg.q2, candPos);
2631 ApplyFastGate(fg);
2632 const double nextCost = stepStartCost + getTotalSwappingCost();
2633 if (nextCost >= boundCost)
2634 continue;
2635
2636 if (stepIdx + 1 >= endStep)
2637 {
2638 boundCost = nextCost;
2639 bestPos = candPos;
2640 }
2641 else
2642 {
2643 const double prevBound = boundCost;
2644 (void)self(self, stepIdx + 1, nextCost, boundCost);
2645 if (boundCost < prevBound)
2646 bestPos = candPos;
2647 }
2648 }
2649
2650 stepState.RestoreTo(*this);
2651 return bestPos;
2652 };
2653
2654 double bestRolloutCost = bestCost;
2655 const IndexType chosenPos = searchAdaptive(searchAdaptive, 0, currentCost, bestRolloutCost);
2656 if (bestRolloutCost < bestCost)
2657 bestCost = bestRolloutCost;
2658 return chosenPos;
2659 }
2660
2661 // Swap two logical qubits so they meet at a specified bond position.
2662 // meetPosition is the bond index (in real/chain coordinates) where
2663 // the two qubits will end up adjacent: one at meetPosition, the other
2664 // at meetPosition+1.
2665 // meetPosition must be in [min(realq1,realq2), max(realq1,realq2)-1].
2666 void SwapQubitsToPosition(IndexType qubit1, IndexType qubit2, IndexType meetPosition)
2667 {
2668 IndexType realq1 = qubitsMap[qubit1];
2669 IndexType realq2 = qubitsMap[qubit2];
2670 if (realq1 > realq2)
2671 {
2672 std::swap(realq1, realq2);
2673 std::swap(qubit1, qubit2);
2674 }
2675
2676 if (realq2 - realq1 <= 1)
2677 return;
2678
2679 assert(meetPosition >= realq1 && meetPosition < realq2);
2680
2681 // Move lower qubit (qubit1) rightward from realq1 to meetPosition
2682 {
2683 IndexType movingReal = realq1;
2684 while (movingReal < meetPosition)
2685 {
2686 const IndexType toReal = movingReal + 1;
2687 const IndexType toInv = qubitsMapInv[toReal];
2688
2689 qubitsMap[toInv] = movingReal;
2690 qubitsMapInv[movingReal] = toInv;
2691
2692 qubitsMap[qubit1] = toReal;
2693 qubitsMapInv[toReal] = qubit1;
2694
2695 totalSwappingCost += bondCost[movingReal];
2696 growBondDimension(movingReal, true);
2697 totalSwappingCost += kPostSwapCostWeight * bondCost[movingReal];
2698 movingReal = toReal;
2699 }
2700 }
2701
2702 // Move upper qubit (qubit2) leftward from realq2 to meetPosition+1
2703 {
2704 IndexType movingReal = realq2;
2705 while (movingReal > meetPosition + 1)
2706 {
2707 const IndexType toReal = movingReal - 1;
2708 const IndexType toInv = qubitsMapInv[toReal];
2709
2710 qubitsMap[toInv] = movingReal;
2711 qubitsMapInv[movingReal] = toInv;
2712
2713 qubitsMap[qubit2] = toReal;
2714 qubitsMapInv[toReal] = qubit2;
2715
2716 totalSwappingCost += bondCost[toReal];
2717 growBondDimension(toReal, true);
2718 totalSwappingCost += kPostSwapCostWeight * bondCost[toReal];
2719 movingReal = toReal;
2720 }
2721 }
2722
2723 assert(abs(qubitsMap[qubit1] - qubitsMap[qubit2]) == 1);
2724 }
2725
2726 private:
2727 struct MemoEntry
2728 {
2729 uint64_t hash = 0;
2730 IndexType bestPos = 0;
2731 double costDelta = 0.0;
2732 bool valid = false;
2733 };
2734
2735 static constexpr size_t kMemoCacheSize = 64;
2736 std::array<MemoEntry, kMemoCacheSize> memoCache{};
2737
2738 size_t nrQubits;
2739
2740 std::vector<IndexType> qubitsMap;
2741 std::vector<IndexType> qubitsMapInv;
2742
2743 size_t physExtent = 2; // 2 for an MPS, 4 for an MPO (operator sites)
2744 IndexType maxVirtualExtent = 0;
2745 std::vector<double> bondCost;
2746 IndexType runtimeBondCap = 0;
2747 double truncationWeight = 0.;
2748 bool swapGatesRelabelQubits = false;
2749 std::vector<double> maxBondDim;
2750 std::vector<double> currentBondDim;
2751
2752 double totalSwappingCost = 0;
2753
2754 double growthFactorSwap = 1.;
2755 double growthFactorGate = 0.7;
2756 double minLayoutGain = 4.; // set per chain type by the constructor
2757
2758 void growBondDimension(IndexType bond, bool swap = true, double schmidtRank = 4)
2759 {
2760 // the left and right bond dimensions are relevant because:
2761 // the initial configuration before applying the swap or other gate is:
2762
2763 // - O - O -
2764 // | |
2765
2766 // The two physical legs have dimension 2
2767 // this is contracted into:
2768
2769 // ---
2770 // -| |-
2771 // ---
2772 // | |
2773
2774 // the left and right dimensions stay the same and also the physical legs have dimension d (2 for an MPS, 4 for an MPO)
2775
2776 // 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
2777 // swap is special, just swaps the values for (0, 1) and (1, 0) in
2778 // the physical legs, while other gates can change all values in the tensor
2779
2780 // then the tensor is reshaped into a matrix, having dimensions d * leftDim x d * rightNeighborDim on this matrix SVD is applied, to separate out the
2781 // qubits tensors again, and the bond dimension is the number of singular
2782 // values kept after truncation (if done), or the number of non-zero
2783 // singular values if no truncation is done. The bond dimension can be at
2784 // most min(d * min(leftDim, rightNeighborDim), maxBondDim[bond]) and the minimum is obviously 1
2785
2786 const IndexType leftBond = bond - 1;
2787 const IndexType rightNeigborBond = bond + 1;
2788 const double betweenDim = currentBondDim[bond];
2789
2790 const double leftDim = leftBond >= 0 ? currentBondDim[leftBond] : 1;
2791 const double rightNeighborDim = rightNeigborBond < static_cast<IndexType>(currentBondDim.size()) ? currentBondDim[rightNeigborBond] : 1;
2792
2793 currentBondDim[bond] = EstimateGrownBondDimension(bond, leftDim, betweenDim, rightNeighborDim, swap, schmidtRank);
2794
2795 bondCost[bond] = BondCost(currentBondDim[bond]);
2796 }
2797};
2798
2799} // namespace Simulators
2800
2801#endif
Circuit class for holding the sequence of operations.
Definition Circuit.h:51
virtual unsigned int GetNumQubits() const =0
Get the number of qubits.
The operation interface.
Definition Operations.h:402
The interface for quantum gates.
virtual QuantumGateType GetGateType() const =0
Get the type of the quantum gate.
virtual std::vector< double > GetParams() const
Get the gate parameters.
void SetCurrentBondDimensions(const std::vector< double > &dims)
void SetRoutingMap(const std::vector< long long int > &actualMap)
void SetSwapGatesRelabelQubits(bool enabled)
const std::vector< IndexType > & getQubitsMap() const
void ApplyTwoQubitGate(IndexType qubit, IndexType controllingQubit1, double schmidtRank, bool isSwapGate=false)
static IndexType EffectiveMapMaxBondDimension(size_t nrQubits, bool operatorChain, IndexType cap)
void setGrowthFactorSwap(double factor)
static constexpr double kPostSwapCostWeight
void RelabelQubits(IndexType q1, IndexType q2)
static bool IsControlledTwoQubitGateType(Circuits::QuantumGateType type)
static double GateSchmidtRank(const Circuits::IQuantumGate<> &gate)
static constexpr double kPostGateCostWeight
const std::vector< IndexType > & getQubitsMapInv() const
static IndexType EffectiveLookaheadMaxBondDimension(size_t nrQubits, bool operatorChain, IndexType cap)
void ApplyGates(const std::vector< QC::Gates::AppliedGate< MatrixClass > > &gates)
const std::vector< double > & getCurrentBondDimensions() const
void RecordAdjacentTwoQubitGate(IndexType bond, bool isSwapGate, double schmidtRank)
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
void SetRoutingCostModel(IndexType cap, double weight)
QC::TensorNetworks::MPSSimulatorInterface::MatrixClass MatrixClass
const std::vector< double > & getMaxBondDimensions() const
IndexType SelectMeetingPositionAdaptive(const std::vector< std::shared_ptr< Circuits::IOperation<> > > &upcomingGates, long long int currentGateIndex, int lookaheadDepth, int lookaheadDepthWithHeuristic, double currentCost, double &bestCost)
void ApplyGates(const std::vector< std::shared_ptr< Circuits::IOperation<> > > &gates)
double BondCost(double dimension) const
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)
MPSDummySimulator(size_t N, bool operatorChain=false)
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)
QuantumGateType
The type of quantum gates.
@ kGate
the usual quantum gate, result stays in simulator's state
Definition Operations.h:30
Bounded online routing based on current physical bond dimensions.
double RoutingSchmidtRank(Circuits::QuantumGateType type, double angle, MatrixFn &&matrix)
bool IsControlledRoutingGate(Circuits::QuantumGateType type)