Maestro 0.3.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
BoundedRoutingPlanner.h
Go to the documentation of this file.
1
9#pragma once
10
12#include "RoutingGateInfo.h"
13
14#include <algorithm>
15#include <array>
16#include <cmath>
17#include <complex>
18#include <cstddef>
19#include <cstdlib>
20#include <limits>
21#include <memory>
22#include <stdexcept>
23#include <utility>
24#include <vector>
25
26namespace Simulators
27{
28namespace Private
29{
30
46{
47 public:
48 using Index = long long;
49 using Map = std::vector<Index>;
50 using Operation = std::shared_ptr<Circuits::IOperation<>>;
51 enum class Policy
52 {
55 };
56
57 private:
58 struct Gate
59 {
60 Index a = 0, b = 0;
61 bool valid = false, swap = false;
62 double rank = 4.;
63 std::vector<std::pair<Index, size_t>> singlesBefore;
64 };
65
66 public:
67 // Only operation-derived values are shared. Each planner owns its physical
68 // cut limits and mutable forecast workspace, so clones can route independently.
70 {
71 private:
74 size_t qubits = 0;
75 std::vector<Gate> gates;
76 std::vector<Index> operationToGate;
77 };
78
79 using SharedOperations = std::shared_ptr<const PreparedOperations>;
80
81 // Applies to Work24 only. Work4 retains its original four-entry horizon.
82 static constexpr size_t kForecastOperationBudget = 4096;
83
84 BoundedRoutingPlanner(size_t n, bool operatorChain, Index cap, const std::vector<Operation> &operations, Policy policy)
85 : BoundedRoutingPlanner(n, operatorChain, cap, PrepareOperations(n, operations), policy)
86 {
87 }
88
89 BoundedRoutingPlanner(size_t n, bool operatorChain, Index cap, SharedOperations operations, Policy policy)
90 : n_(n), operatorChain_(operatorChain), policy_(policy), operations_(ValidateOperations(n, std::move(operations))), gates_(operations_->gates),
91 operationToGate_(operations_->operationToGate)
92 {
93 if (cap < 0)
94 throw std::invalid_argument("Invalid bounded routing configuration");
95 Allocate(initial_);
96 Allocate(scratch_);
97 maximum_.resize(n - 1);
98 const double d = operatorChain ? 4. : 2.;
99 const double limit = static_cast<double>(std::numeric_limits<Index>::max());
100 const double physical = cap > 0 ? static_cast<double>(cap) : limit;
101 for (size_t b = 0; b + 1 < n; ++b)
102 maximum_[b] = CutMaximum(std::min(b + 1, n - b - 1), d, physical);
103 }
104
106 {
107 return operations_;
108 }
109
110 static SharedOperations PrepareOperations(size_t n, const std::vector<Operation> &operations)
111 {
112 if (n < 2 || n > static_cast<size_t>(std::numeric_limits<Index>::max()))
113 throw std::invalid_argument("Invalid bounded routing qubit count");
114 auto prepared = std::make_shared<PreparedOperations>();
115 prepared->qubits = n;
116 prepared->operationToGate.assign(operations.size(), -1);
117 std::vector<size_t> singles(n, 0);
118 std::vector<Index> touchedSingles;
119 for (size_t i = 0; i < operations.size(); ++i)
120 {
121 const auto &operation = operations[i];
122 Gate item;
123 if (operation && operation->GetType() == Circuits::OperationType::kGate)
124 {
125 const auto qubits = operation->AffectedQubits();
126 const bool validQubits = std::all_of(qubits.begin(), qubits.end(), [n](auto q) { return q < n; });
127 if (validQubits && qubits.size() == 1)
128 {
129 // A majority-length list is a sentinel: the dense path
130 // scans and resets every counter, so later indices are unused.
131 if (!singles[qubits[0]] && touchedSingles.size() <= n / 2)
132 touchedSingles.push_back(static_cast<Index>(qubits[0]));
133 ++singles[qubits[0]];
134 continue;
135 }
136 if (validQubits && qubits.size() == 2 && qubits[0] != qubits[1])
137 {
138 item.a = qubits[0];
139 item.b = qubits[1];
140 item.valid = true;
141 const auto info = std::dynamic_pointer_cast<RoutingGateInfo>(operation);
142 const auto gate = std::dynamic_pointer_cast<Circuits::IQuantumGate<>>(operation);
143 item.swap = (info && info->isSwap) ||
144 (gate && (gate->GetGateType() == Circuits::QuantumGateType::kSwapGateType || ExactSwapMatrix(gate->GetMatrix())));
145 item.rank = info ? info->schmidtRank : gate ? GateSchmidtRank(*gate) : 4.;
146 prepared->operationToGate[i] = static_cast<Index>(prepared->gates.size());
147 }
148 }
149 const auto emitSingle = [&item, &singles](Index q) {
150 item.singlesBefore.emplace_back(q, singles[q]);
151 singles[q] = 0;
152 };
153 // Both paths preserve ascending accumulation order. For a dense
154 // prefix, scanning visits fewer than two counters per emitted entry;
155 // sparse prefixes avoid a full-qubit scan.
156 if (touchedSingles.size() > n / 2)
157 {
158 for (size_t q = 0; q < n; ++q)
159 if (singles[q])
160 emitSingle(static_cast<Index>(q));
161 }
162 else
163 {
164 std::sort(touchedSingles.begin(), touchedSingles.end());
165 for (const auto q : touchedSingles)
166 emitSingle(q);
167 }
168 touchedSingles.clear();
169 prepared->gates.push_back(std::move(item));
170 }
171 return prepared;
172 }
173
175 template <class Bond> Index Select(const Map &map, const Bond *bonds, size_t bondCount, size_t operationIndex)
176 {
177 const Index entry = operationIndex < operationToGate_.size() ? operationToGate_[operationIndex] : -1;
178 if (entry < 0 || !gates_[entry].valid || gates_[entry].swap || !Seed(map, bonds, bondCount))
179 return -1;
180 const auto &gate = gates_[entry];
181 const Index low = std::min(map[gate.a], map[gate.b]);
182 const Index high = std::max(map[gate.a], map[gate.b]);
183 if (high - low <= 1)
184 return low;
185
186 const auto candidates = CandidatesFor(entry);
187 const size_t horizon = policy_ == Policy::Work4 ? 4 : CommonHorizon(entry, candidates.count);
188 Index chosen = candidates.positions[0];
189 double best = std::numeric_limits<double>::infinity();
190 if (horizon)
191 for (size_t i = 0; i < candidates.count; ++i)
192 {
193 const double score = Evaluate(entry, candidates.positions[i], horizon, best);
194 if (score < best)
195 {
196 best = score;
197 chosen = candidates.positions[i];
198 }
199 }
200 return chosen;
201 }
202
203 template <class Bond> Index Select(const Map &map, const std::vector<Bond> &bonds, size_t operationIndex)
204 {
205 return Select(map, bonds.data(), bonds.size(), operationIndex);
206 }
207
208 private:
209 struct State
210 {
211 Map map, inverse;
212 std::vector<double> bonds;
213 };
214
215 struct CandidateSet
216 {
217 std::array<Index, 5> positions{};
218 size_t count = 0;
219 };
220
221 size_t n_;
222 bool operatorChain_;
223 Policy policy_;
224 SharedOperations operations_;
225 const std::vector<Gate> &gates_;
226 const std::vector<Index> &operationToGate_;
227 std::vector<double> maximum_;
228 State initial_, scratch_;
229
231
232 static SharedOperations ValidateOperations(size_t n, SharedOperations operations)
233 {
234 if (n < 2 || n > static_cast<size_t>(std::numeric_limits<Index>::max()) || !operations || operations->qubits != n)
235 throw std::invalid_argument("Incompatible bounded routing operation table");
236 return operations;
237 }
238
239 static double Cube(double x)
240 {
241 return x * x * x;
242 }
243
244 static double CutMaximum(size_t sites, double d, double cap)
245 {
246 double result = 1.;
247 for (size_t k = 0; k < sites && result < cap; ++k)
248 result = std::min(cap, result * d);
249 return result;
250 }
251
252 static double GateSchmidtRank(const Circuits::IQuantumGate<> &gate)
253 {
254 if (gate.GetNumQubits() != 2)
255 return 4.;
256 const auto params = gate.GetParams();
257 return RoutingSchmidtRank(gate.GetGateType(), params.empty() ? 0. : params[0], [&gate] { return gate.GetMatrix(); });
258 }
259
260 static bool ExactSwapMatrix(const Eigen::MatrixXcd &matrix)
261 {
262 if (matrix.rows() != 4 || matrix.cols() != 4)
263 return false;
264 for (int row = 0; row < 4; ++row)
265 for (int col = 0; col < 4; ++col)
266 if (matrix(row, col) != std::complex<double>(row == (((col & 1) << 1) | (col >> 1)) ? 1. : 0., 0.))
267 return false;
268 return true;
269 }
270
271 void Allocate(State &state)
272 {
273 state.map.resize(n_);
274 state.inverse.resize(n_);
275 state.bonds.resize(n_ - 1);
276 }
277
278 template <class Bond> bool Seed(const Map &map, const Bond *bonds, size_t bondCount)
279 {
280 if (map.size() != n_ || bondCount != n_ - 1 || bonds == nullptr)
281 return false;
282 std::fill(initial_.inverse.begin(), initial_.inverse.end(), -1);
283 for (size_t q = 0; q < n_; ++q)
284 {
285 if (map[q] < 0 || static_cast<size_t>(map[q]) >= n_ || initial_.inverse[map[q]] != -1)
286 return false;
287 initial_.map[q] = map[q];
288 initial_.inverse[map[q]] = static_cast<Index>(q);
289 }
290 for (size_t b = 0; b < bondCount; ++b)
291 {
292 const double rank = static_cast<double>(bonds[b]);
293 if (!std::isfinite(rank) || rank < 1.)
294 return false;
295 initial_.bonds[b] = rank;
296 }
297 return true;
298 }
299
300 // The installed native local routing policy. This also chooses every
301 // future meeting position in a forecast; only the first meeting varies.
302 static Index LocalMeeting(const std::vector<double> &dims, Index low, Index high, bool operatorChain)
303 {
304 const Index span = high - low;
305 if (span <= 1)
306 return low;
307 std::array<double, 130> bonds{};
308 std::array<double, 128> prefixes{}, suffixes{};
309 std::vector<double> heap;
310 double *b = bonds.data(), *p = prefixes.data(), *s = suffixes.data();
311 if (span > 128)
312 {
313 heap.assign(static_cast<size_t>(3 * span + 2), 0.);
314 b = heap.data();
315 p = b + span + 2;
316 s = p + span;
317 }
318 b[0] = low ? dims[low - 1] : 1.;
319 for (Index i = 0; i < span; ++i)
320 b[i + 1] = dims[low + i];
321 b[span + 1] = high < static_cast<Index>(dims.size()) ? dims[high] : 1.;
322 const double d = operatorChain ? 4. : 2.;
323 const double e1 = std::max(b[0], b[1]), e2 = std::max(b[span], b[span + 1]);
324 for (Index i = 0; i < span - 1; ++i)
325 p[i + 1] = p[i] + std::max(0., Cube(std::min(e1, d * b[i + 1])) - Cube(b[i + 1]));
326 for (Index i = span - 1; i > 0; --i)
327 s[i - 1] = s[i] + std::max(0., Cube(std::min(e2, d * b[i + 1])) - Cube(b[i + 1]));
328 Index chosen = low;
329 double best = std::numeric_limits<double>::infinity();
330 for (Index i = 0; i < span; ++i)
331 {
332 const double tie = 1e-6 * (b[span] * b[span + 1] > b[0] * b[1] ? span - 1 - i : i);
333 const double score = Cube(b[i + 1]) + .5 * (p[i] + s[i]) + .05 * b[i] * b[i + 1] * b[i + 2] + tie;
334 if (score < best)
335 {
336 best = score;
337 chosen = low + i;
338 }
339 }
340 return chosen;
341 }
342
343 CandidateSet CandidatesFor(Index entry) const
344 {
345 CandidateSet out;
346 const auto &gate = gates_[entry];
347 const Index low = std::min(initial_.map[gate.a], initial_.map[gate.b]);
348 const Index high = std::max(initial_.map[gate.a], initial_.map[gate.b]);
349 const auto add = [&out](Index position) {
350 for (size_t i = 0; i < out.count; ++i)
351 if (out.positions[i] == position)
352 return;
353 out.positions[out.count++] = position;
354 };
355 // Ordering is part of the policy: exact score ties keep the first.
356 add(LocalMeeting(initial_.bonds, low, high, operatorChain_));
357 add(low);
358 add(high - 1);
359 add(low + (high - low) / 2);
360 Index minimum = low;
361 for (Index b = low + 1; b < high; ++b)
362 if (initial_.bonds[b] < initial_.bonds[minimum])
363 minimum = b;
364 add(minimum);
365 return out;
366 }
367
368 size_t CommonHorizon(Index entry, size_t candidates) const
369 {
370 size_t steps = 0, upperBound = 0;
371 const size_t perCandidateBudget = kForecastOperationBudget / candidates;
372 for (size_t step = 0; step < 24 && static_cast<size_t>(entry) + step < gates_.size(); ++step)
373 {
374 const auto &gate = gates_[static_cast<size_t>(entry) + step];
375 if (!gate.valid)
376 break;
377 // Count physical two-site updates, logical SWAPs and aggregated
378 // single-qubit updates. All candidates receive the same prefix.
379 size_t updates = gate.swap ? 1 : n_ - 1;
380 if (step == 0 && !gate.swap)
381 updates = static_cast<size_t>(std::abs(initial_.map[gate.a] - initial_.map[gate.b]));
382 const size_t singles = step ? gate.singlesBefore.size() : 0;
383 if (updates > perCandidateBudget - upperBound || singles > perCandidateBudget - upperBound - updates)
384 break;
385 upperBound += updates + singles;
386 ++steps;
387 }
388 return steps;
389 }
390
391 double Grow(double left, double middle, double right, double maximum, bool swap, double gateRank) const
392 {
393 const double d = operatorChain_ ? 4. : 2., rank = operatorChain_ ? gateRank * gateRank : gateRank;
394 if (swap)
395 {
396 if (left == 1. && middle == 1. && right == 1.)
397 return 1.;
398 return std::max(1., std::min({d * left, d * right, rank * middle, maximum}));
399 }
400 const auto at = [&](double r) { return std::max(1., std::min(.7 * std::min(d * std::min(left, right), middle * r), maximum)); };
401 return rank >= 2. ? at(rank) : middle + (std::max(rank, 1.) - 1.) * (at(2.) - middle);
402 }
403
404 double StepWork(double left, double middle, double right, double post, bool swap) const
405 {
406 const double d = operatorChain_ ? 4. : 2.;
407 if (swap && left == 1. && middle == 1. && right == 1.)
408 return 4. * d;
409 const double m = d * left, n = d * right;
410 return m * n * (middle + std::min(m, n) + post);
411 }
412
413 void Relabel(Index a, Index b)
414 {
415 std::swap(scratch_.map[a], scratch_.map[b]);
416 scratch_.inverse[scratch_.map[a]] = a;
417 scratch_.inverse[scratch_.map[b]] = b;
418 }
419
420 void Adjacent(Index bond, bool swap, double gateRank, double &work)
421 {
422 const double left = bond ? scratch_.bonds[bond - 1] : 1., middle = scratch_.bonds[bond];
423 const double right = bond + 1 < static_cast<Index>(n_ - 1) ? scratch_.bonds[bond + 1] : 1.;
424 const double post = Grow(left, middle, right, maximum_[bond], swap, gateRank);
425 work += StepWork(left, middle, right, post, swap);
426 scratch_.bonds[bond] = post;
427 if (swap)
428 Relabel(scratch_.inverse[bond], scratch_.inverse[bond + 1]);
429 }
430
431 double Evaluate(Index entry, Index meeting, size_t horizon, double incumbent)
432 {
433 scratch_ = initial_;
434 double work = 0.;
435 const double d = operatorChain_ ? 4. : 2.;
436 for (size_t step = 0; step < horizon && static_cast<size_t>(entry) + step < gates_.size(); ++step)
437 {
438 const auto &gate = gates_[static_cast<size_t>(entry) + step];
439 if (!gate.valid)
440 break;
441 if (step)
442 for (const auto &single : gate.singlesBefore)
443 {
444 const Index position = scratch_.map[single.first];
445 const double left = position ? scratch_.bonds[position - 1] : 1.;
446 const double right = position < static_cast<Index>(n_ - 1) ? scratch_.bonds[position] : 1.;
447 work += static_cast<double>(single.second) * d * d * left * right;
448 }
449 if (gate.swap)
450 {
451 Relabel(gate.a, gate.b);
452 continue;
453 }
454 const Index low = std::min(scratch_.map[gate.a], scratch_.map[gate.b]);
455 const Index high = std::max(scratch_.map[gate.a], scratch_.map[gate.b]);
456 const Index meet = step == 0 ? meeting : LocalMeeting(scratch_.bonds, low, high, operatorChain_);
457 // Native routing moves the lower endpoint right before moving the
458 // upper endpoint left. Each step sees the updated adjacent ranks.
459 for (Index bond = low; bond < meet; ++bond)
460 Adjacent(bond, true, 4., work);
461 for (Index bond = high - 1; bond > meet; --bond)
462 Adjacent(bond, true, 4., work);
463 Adjacent(meet, false, gate.rank, work);
464 // Remaining work terms are nonnegative. A candidate that cannot
465 // strictly improve the incumbent also cannot win its stable tie.
466 if (work >= incumbent)
467 return work;
468 }
469 return work;
470 }
471};
472
473} // namespace Private
474} // namespace Simulators
virtual unsigned int GetNumQubits() const =0
Get the number of qubits.
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.
std::shared_ptr< Circuits::IOperation<> > Operation
BoundedRoutingPlanner(size_t n, bool operatorChain, Index cap, const std::vector< Operation > &operations, Policy policy)
BoundedRoutingPlanner(size_t n, bool operatorChain, Index cap, SharedOperations operations, Policy policy)
Index Select(const Map &map, const std::vector< Bond > &bonds, size_t operationIndex)
std::shared_ptr< const PreparedOperations > SharedOperations
static SharedOperations PrepareOperations(size_t n, const std::vector< Operation > &operations)
Index Select(const Map &map, const Bond *bonds, size_t bondCount, size_t operationIndex)
Return a physical meeting bond, or -1 to use the native fallback.
const SharedOperations & GetPreparedOperations() const noexcept
@ 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)