Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
N_Qubit_Decomposition_Tree_Search.cpp
Go to the documentation of this file.
1 /*
2 Created on Fri Jun 26 14:13:26 2020
3 Copyright 2020 Peter Rakyta, Ph.D.
4 
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8 
9  http://www.apache.org/licenses/LICENSE-2.0
10 
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 
17 @author: Peter Rakyta, Ph.D.
18 */
24 
26 #include "n_aryGrayCodeCounter.h"
27 
28 #include <algorithm>
29 #include <chrono>
30 #include <cmath>
31 #include <iostream>
32 #include <numeric>
33 #include <queue>
34 #include <random>
35 #include <stdlib.h>
36 #include <thread>
37 #include <time.h>
38 #include <unordered_map>
39 
44 struct LevelResult {
46  std::set<std::vector<int>> visited;
48  std::map<std::vector<int>, GrayCodeCNOT> seq_pairs_of;
50  std::vector<std::pair<std::vector<int>, GrayCodeCNOT>> out_res;
51 };
52 
53 using Discovery = std::vector<std::pair<std::vector<int>, GrayCodeCNOT>>;
54 
73 
74  std::vector<int> I(n, 0);
75  for (int i = 0; i < n; ++i)
76  I[i] = 1 << i;
77  std::set<std::vector<int>> visited;
78  visited.emplace(I);
79  std::map<std::vector<int>, GrayCodeCNOT> seq_pairs_of;
80  seq_pairs_of.emplace(I, GrayCodeCNOT{});
81  // emit the root
83  out_res.emplace_back(I, GrayCodeCNOT{});
84 
86  result.visited = std::move(visited);
87  result.seq_pairs_of = std::move(seq_pairs_of);
88  result.out_res = std::move(out_res);
89  return result;
90 }
91 
92 // Return true iff 'seq' (list of CNOT pairs) equals the canonical
93 // Kahn topological order under the tie-breaker: lexicographic by pair,
94 // then by original index (to stabilize identical pairs).
95 static int canonical_prefix_ok(const GrayCodeCNOT& path, const std::vector<matrix_base<int>>& topology) {
96  const int m = static_cast<int>(path.size());
97  if (m <= 1)
98  return -1;
99 
100  // 2) per-qubit serial constraints: edge u->v if ops u,v share a qubit and u < v
101  std::vector<std::vector<int>> succ(m);
102  std::vector<int> indeg(m, 0);
103  std::unordered_map<int, int> last_on; // qubit -> last op index touching it
104  last_on.reserve(m * 2);
105 
106  for (int k = 0; k < m; ++k) {
107  const int a = topology[path.data[k]][0];
108  const int b = topology[path.data[k]][1];
109  for (int q : {a, b}) {
110  std::unordered_map<int, int>::iterator it = last_on.find(q);
111  if (it != last_on.end()) {
112  int prev = it->second;
113  succ[prev].push_back(k);
114  ++indeg[k];
115  it->second = k;
116  } else {
117  last_on.emplace(q, k);
118  }
119  }
120  }
121 
122  // 3) deterministic Kahn with min-heap by (pair, index)
123  struct Node {
124  std::pair<int, int> p;
125  int idx;
126  };
127  struct Cmp {
128  bool operator()(const Node& a, const Node& b) const {
129  if (a.p != b.p)
130  return a.p > b.p; // lexicographically smaller first
131  return a.idx > b.idx; // then by original index
132  }
133  };
134  std::priority_queue<Node, std::vector<Node>, Cmp> pq;
135  for (int k = 0; k < m; ++k)
136  if (indeg[k] == 0)
137  pq.push(Node{std::make_pair(topology[path.data[k]][0], topology[path.data[k]][1]), k});
138 
139  // 4) walk canonical order and require it matches the given prefix exactly
140  for (int pos = 0; pos < m; ++pos) {
141  if (pq.empty())
142  return pos; // malformed (shouldn’t happen)
143  Node u = pq.top();
144  pq.pop();
145  if (u.idx != pos)
146  return pos; // deviation: not canonical
147 
148  for (int v : succ[u.idx]) {
149  if (--indeg[v] == 0)
150  pq.push(Node{std::make_pair(topology[path.data[v]][0], topology[path.data[v]][1]), v});
151  }
152  }
153  return -1;
154 }
155 
156 static int is_unique_structure(const GrayCodeCNOT& path, const std::vector<matrix_base<int>>& topology) {
157  for (int idx = 0; idx < path.size() - 3; idx++) {
158  if (path.data[idx] == path.data[idx + 1] && path.data[idx] == path.data[idx + 2] && path.data[idx] == path.data[idx + 3]) {
159  return false; // avoid more than 3 repeated CNOTs
160  }
161  }
162  return canonical_prefix_ok(path, topology) < 0; // not canonical prefix
163 }
164 
194  const std::vector<matrix_base<int>>& topology,
195  bool use_gl = true) {
196  std::set<std::vector<int>>& visited = L.visited;
197  std::map<std::vector<int>, GrayCodeCNOT>& seq_pairs_of = L.seq_pairs_of;
198  std::vector<std::vector<int>>& q = L.q;
199  std::map<std::vector<int>, GrayCodeCNOT> new_seq_pairs_of;
201  while (!q.empty()) {
202 
203  std::vector<int> A = q.back();
204  q.pop_back();
205 
206  const GrayCodeCNOT& last_pairs = seq_pairs_of.at(A);
207  for (int p = 0; p < (int)topology.size(); ++p) {
208  // try both directions
209  // ensure p is unordered i<j; assume caller provides that
210  std::pair<int, int> m1 = {topology[p][0], topology[p][1]};
211  std::pair<int, int> m2 = {topology[p][1], topology[p][0]};
212 
213  if (!use_gl) {
214  if (last_pairs.size() >= 3 &&
215  std::all_of(last_pairs.data + last_pairs.size() - 3, last_pairs.data + last_pairs.size(),
216  [p](const int& x) { return x == p; }))
217  continue; // avoid more than 3 repeated CNOTs
218  GrayCodeCNOT seqp = last_pairs.add_Digit(static_cast<int>(topology.size()));
219  seqp[seqp.size() - 1] = p;
220  if (canonical_prefix_ok(seqp, topology) >= 0)
221  continue; // not canonical prefix
222  }
223 
224  std::vector<std::pair<int, int>> allmv =
225  use_gl ? std::vector<std::pair<int, int>>{m1, m2} : std::vector<std::pair<int, int>>{m1};
226 
227  for (std::pair<int, int> mv : allmv) {
228  std::vector<int> B;
229  if (use_gl) {
230  B = A;
231  if (mv.first != mv.second) {
232  B[mv.second] ^= B[mv.first];
233  }
234 
235  if (visited.find(B) != visited.end()) {
236  continue; // discovered already (at minimal or earlier depth)
237  }
238  } else {
239  B = std::vector<int>(last_pairs.data, last_pairs.data + last_pairs.size());
240  B.push_back(p);
241  }
242  visited.emplace(B);
243 
244  // build sequences
245  GrayCodeCNOT seqp = last_pairs.add_Digit(static_cast<int>(topology.size()));
246  seqp[seqp.size() - 1] = p;
247 
248  new_seq_pairs_of.emplace(B, std::move(seqp));
249 
250  // emit discovery: (depth+1, B, seq_pairs_of[B], seq_dir_of[B])
251  const GrayCodeCNOT& ref_pairs = new_seq_pairs_of.at(B);
252  out_res.emplace_back(std::move(B), ref_pairs);
253  }
254  }
255  }
257  result.visited = std::move(visited);
258  result.seq_pairs_of = std::move(new_seq_pairs_of);
259  result.out_res = std::move(out_res);
260  return result;
261 }
262 
263 
264 template <class Callback>
266  const GrayCodeCNOT& curpath,
267  const std::vector<matrix_base<int>>& topology,
268  const std::vector<int>& topo_filt,
269  int num_cnot,
270  std::vector<int>& places,
271  std::vector<int>& pairs,
272  int depth,
273  int min_place,
274  Callback&& callback,
275  bool & early_stop)
276 {
277  const int nslots = curpath.size() + 1;
278 
279  if (depth == num_cnot) {
280  matrix_base<int8_t> limits = matrix_base<int8_t>(1, curpath.size()+num_cnot);
281  std::fill(limits.data, limits.data + limits.size(), static_cast<int8_t>(topology.size()));
282  GrayCodeCNOT out(limits);
283 
284  int j = 0, k = 0;
285  for (int slot = 0; slot < nslots; ++slot) {
286  while (j < num_cnot && places[j] == slot) {
287  if (k > 2 && out[k-1] == pairs[j] && out[k-2] == pairs[j] && out[k-3] == pairs[j]) {
288  return; // avoid more than 3 repeated CNOTs
289  }
290  out[k++] = pairs[j];
291  ++j;
292  }
293  if (slot < curpath.size()) {
294  if (k > 2 && out[k-1] == curpath[slot] && out[k-2] == curpath[slot] && out[k-3] == curpath[slot]) {
295  return; // avoid more than 3 repeated CNOTs
296  }
297  out[k++] = curpath[slot];
298  }
299  }
300  early_stop |= callback(out);
301  return;
302  }
303  uint32_t used_mask = 0u;
304  for (int d = 0; d < depth; d++) {
305  used_mask |= (1u<<topology[pairs[d]][0]) | (1u<<topology[pairs[d]][1]);
306  }
307  for (int place = min_place; place < nslots; ++place) {
308  if (depth != 0 && places[depth-1]+1 < place) {
309  continue; // avoid insertions more than one place away
310  }
311  places[depth] = place;
312  for (int topo_idx : topo_filt) {
313  uint32_t edge_mask = (1u<<topology[topo_idx][0]) | (1u<<topology[topo_idx][1]);
314  if (depth != 0 && (used_mask & edge_mask) == 0) continue;
315  pairs[depth] = topo_idx;
317  curpath, topology, topo_filt, num_cnot,
318  places, pairs, depth + 1, place,
319  callback, early_stop);
320  if (early_stop) return;
321  }
322  }
323 }
324 
325 template <class Callback>
327  const GrayCodeCNOT& curpath,
328  const std::vector<matrix_base<int>>& topology,
329  const std::vector<int>& topo_filt,
330  int num_cnot,
331  Callback&& callback)
332 {
333  std::vector<int> places(num_cnot);
334  std::vector<int> pairs(num_cnot);
335  bool early_stop = false;
337  curpath, topology, topo_filt, num_cnot,
338  places, pairs, 0, 0,
339  std::forward<Callback>(callback), early_stop);
340 }
341 
347 
348  // set the level limit
349  level_limit = 0;
350 
351  // BFGS is better for smaller problems, while ADAM for larger ones
352  if (qbit_num <= 5) {
354 
355  // Maximal number of iterations in the optimization process
357  max_inner_iterations = 10000;
358  } else {
360 
361  // Maximal number of iterations in the optimization process
363  }
364 }
365 
375  std::map<std::string, Config_Element>& config,
376  int accelerator_num)
377  : N_Qubit_Decomposition_Tree_Search(Umtx_in, qbit_num_in, {}, config, accelerator_num) {}
378 
383  std::map<std::string, Config_Element>& config,
384  int accelerator_num)
385  : N_Qubit_Decomposition_Tree_Search(Umtx_in, qbit_num_in, {}, config, accelerator_num) {}
386 
397  std::vector<matrix_base<int>> topology_in,
398  std::map<std::string, Config_Element>& config,
399  int accelerator_num)
400  : Optimization_Interface(Umtx_in, qbit_num_in, false, config, RANDOM, accelerator_num) {
401 
402  // set the level limit
403  level_limit = 0;
404 
405  // Maximal number of iterations in the optimization process
407 
408  // setting the topology
409  topology = topology_in;
410 
411  if (topology.size() == 0) {
412  for (int qbit1 = 0; qbit1 < qbit_num; qbit1++) {
413  for (int qbit2 = qbit1 + 1; qbit2 < qbit_num; qbit2++) {
414  matrix_base<int> edge(2, 1);
415  edge[0] = qbit1;
416  edge[1] = qbit2;
417 
418  topology.push_back(edge);
419  }
420  }
421  } else {
422  for (size_t idx = 0; idx < topology.size(); idx++) {
423  if (topology[idx].size() != 2) {
424  std::string error("invalid topology: each element should be a pair of integers");
425  throw error;
426  }
427  if (topology[idx][0] < 0 || topology[idx][0] >= qbit_num || topology[idx][1] < 0 || topology[idx][1] >= qbit_num) {
428  std::string error("invalid topology: qubit indices should be between 0 and qbit_num-1");
429  throw error;
430  }
431  if (topology[idx][0] == topology[idx][1]) {
432  std::string error("invalid topology: target and control qubits should be different");
433  throw error;
434  }
435  if (topology[idx][0] > topology[idx][1]) {
436  std::swap(topology[idx][0], topology[idx][1]);
437  }
438  }
439  }
440 
441  // construct the possible CNOT combinations within a single level
442  // the number of possible CNOT connections netween the qubits (including topology constraints)
443  int n_ary_limit_max = static_cast<int>(topology.size());
444 
445  possible_target_qbits = matrix_base<int>(1, n_ary_limit_max);
446  possible_control_qbits = matrix_base<int>(1, n_ary_limit_max);
447  for (int element_idx = 0; element_idx < n_ary_limit_max; element_idx++) {
448 
449  matrix_base<int>& edge = topology[element_idx];
450  possible_target_qbits[element_idx] = edge[0];
451  possible_control_qbits[element_idx] = edge[1];
452  }
453 
454  // BFGS is better for smaller problems, while ADAM for larger ones
455  if (qbit_num <= 5) {
456  alg = BFGS;
457 
458  // Maximal number of iterations in the optimization process
460  max_inner_iterations = 10000;
461  } else {
462  alg = ADAM;
463 
464  // Maximal number of iterations in the optimization process
466  }
467 }
468 
473  std::vector<matrix_base<int>> topology_in,
474  std::map<std::string, Config_Element>& config,
475  int accelerator_num)
476  : Optimization_Interface(Umtx_in, qbit_num_in, false, config, RANDOM, accelerator_num) {
477 
478  // set the level limit
479  level_limit = 0;
480 
481  // Maximal number of iterations in the optimization process
483 
484  // setting the topology
485  topology = topology_in;
486 
487  if (topology.size() == 0) {
488  for (int qbit1 = 0; qbit1 < qbit_num; qbit1++) {
489  for (int qbit2 = qbit1 + 1; qbit2 < qbit_num; qbit2++) {
490  matrix_base<int> edge(2, 1);
491  edge[0] = qbit1;
492  edge[1] = qbit2;
493 
494  topology.push_back(edge);
495  }
496  }
497  } else {
498  for (size_t idx = 0; idx < topology.size(); idx++) {
499  if (topology[idx].size() != 2) {
500  std::string error("invalid topology: each element should be a pair of integers");
501  throw error;
502  }
503  if (topology[idx][0] < 0 || topology[idx][0] >= qbit_num || topology[idx][1] < 0 || topology[idx][1] >= qbit_num) {
504  std::string error("invalid topology: qubit indices should be between 0 and qbit_num-1");
505  throw error;
506  }
507  if (topology[idx][0] == topology[idx][1]) {
508  std::string error("invalid topology: target and control qubits should be different");
509  throw error;
510  }
511  if (topology[idx][0] > topology[idx][1]) {
512  std::swap(topology[idx][0], topology[idx][1]);
513  }
514  }
515  }
516 
517  // construct the possible CNOT combinations within a single level
518  // the number of possible CNOT connections netween the qubits (including topology constraints)
519  int n_ary_limit_max = static_cast<int>(topology.size());
520 
521  possible_target_qbits = matrix_base<int>(1, n_ary_limit_max);
522  possible_control_qbits = matrix_base<int>(1, n_ary_limit_max);
523  for (int element_idx = 0; element_idx < n_ary_limit_max; element_idx++) {
524 
525  matrix_base<int>& edge = topology[element_idx];
526  possible_target_qbits[element_idx] = edge[0];
527  possible_control_qbits[element_idx] = edge[1];
528  }
529 
530  // BFGS is better for smaller problems, while ADAM for larger ones
531  if (qbit_num <= 5) {
532  alg = BFGS;
533 
534  // Maximal number of iterations in the optimization process
536  max_inner_iterations = 10000;
537  } else {
538  alg = ADAM;
539 
540  // Maximal number of iterations in the optimization process
542  }
543 }
544 
549 
556 
557  // The string stream input to store the output messages.
558  std::stringstream sstream;
559  sstream << "***************************************************************" << std::endl;
560  sstream << "Starting to disentangle " << qbit_num << "-qubit matrix" << std::endl;
561  sstream << "***************************************************************" << std::endl << std::endl << std::endl;
562 
563  print(sstream, 1);
564 
565 // temporarily turn off OpenMP parallelism
566 #if BLAS == 0 // undefined BLAS
569 #elif BLAS == 1 // MKL
570  num_threads = mkl_get_max_threads();
571  MKL_Set_Num_Threads(1);
572 #elif BLAS == 2 // OpenBLAS
573  num_threads = openblas_get_num_threads();
574  openblas_set_num_threads(1);
575 #endif
576 
578 
579  long long export_circuit_2_binary_loc;
580  if (config.count("export_circuit_2_binary") > 0) {
581  config["export_circuit_2_binary"].get_property(export_circuit_2_binary_loc);
582  } else {
583  export_circuit_2_binary_loc = 0;
584  }
585 
586  if (export_circuit_2_binary_loc > 0) {
587  std::string filename("circuit_squander.binary");
588  if (project_name != "") {
589  filename = project_name + "_" + filename;
590  }
591  export_gate_list_to_binary(optimized_parameters_mtx, gate_structure_loc, filename, verbose);
592 
593  std::string unitaryname("unitary_squander.binary");
594  if (project_name != "") {
595  filename = project_name + "_" + unitaryname;
596  }
597  export_unitary(unitaryname);
598  }
599 
600  // store the created gate structure
601  release_gates();
602  combine(gate_structure_loc);
603  delete (gate_structure_loc);
604 
606 
607 #if BLAS == 0 // undefined BLAS
609 #elif BLAS == 1 // MKL
610  MKL_Set_Num_Threads(num_threads);
611 #elif BLAS == 2 // OpenBLAS
612  openblas_set_num_threads(num_threads);
613 #endif
614 }
615 
622 
623  double optimization_tolerance_loc;
624  long long level_max = 14;
625  if (config.count("optimization_tolerance") > 0) {
626  config["optimization_tolerance"].get_property(optimization_tolerance_loc);
627 
628  } else {
629  optimization_tolerance_loc = optimization_tolerance;
630  }
631 
632  if (config.count("tree_level_max") > 0) {
633  config["tree_level_max"].get_property(level_max);
634  }
635  long long use_osr = 1;
636  if (config.count("use_osr") > 0) {
637  config["use_osr"].get_property(use_osr);
638  }
639  long long use_graph_search = 1;
640  if (config.count("use_graph_search") > 0) {
641  config["use_graph_search"].get_property(use_graph_search);
642  }
643 
644  long long stop_first_solution = 1;
645  if (config.count("stop_first_solution") > 0) {
646  config["stop_first_solution"].get_property(stop_first_solution);
647  }
648 
649  level_limit = std::min(std::max((int)level_max, 0), 14);
650 
651  if (level_limit < 0) {
652  std::string error("please increase level limit");
653  throw error;
654  }
655 
656  GrayCodeCNOT best_solution;
657  std::vector<GrayCodeCNOT> all_solutions;
658  if (use_graph_search) {
659  all_solutions.emplace_back(tree_search_over_gate_structures_best_first());
660  } else {
661 
662  double minimum_best_solution = current_minimum;
663  LevelInfo li;
664  std::vector<std::vector<int>> all_cuts = unique_cuts(qbit_num);
665  std::sort(all_cuts.begin(), all_cuts.end(), [](const std::vector<int>& a, const std::vector<int>& b){
666  if (a.size() != b.size()) return a.size() < b.size();
667  return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
668  });
669  std::map<std::pair<int, int>, std::vector<int>> pair_affects;
670  for (const matrix_base<int>& pair : topology) {
671  std::vector<int> cuts;
672  for (size_t i = 0; i < all_cuts.size(); ++i) {
673  const std::vector<int>& A = all_cuts[i];
674  if ((std::find(A.begin(), A.end(), pair[0]) != A.end()) ^
675  (std::find(A.begin(), A.end(), pair[1]) != A.end())) {
676  cuts.push_back(static_cast<int>(i));
677  }
678  }
679  pair_affects[std::pair<int, int>(pair[0], pair[1])] = std::move(cuts);
680  }
681  CutInfo ci(std::move(all_cuts), MinCnotBoundSolver(qbit_num, all_cuts, topology));
682 
683  for (int level = 0; level <= level_limit; level++) {
684  GrayCodeCNOT gcode;
685  if (use_osr) {
686  if (qbit_num <= 1) {
687  all_solutions.emplace_back();
688  break;
689  } else {
691  all_solutions.insert(all_solutions.end(), result.solutions.begin(), result.solutions.end());
692  std::swap(li, result.level_info);
693  ci.prefixes = std::move(result.prefixes);
694  }
695  if (stop_first_solution && all_solutions.size() > 0) {
696  break;
697  }
698  } else {
699  gcode = std::move(tree_search_over_gate_structures(level));
700  if (current_minimum < minimum_best_solution) {
701 
702  minimum_best_solution = current_minimum;
703  best_solution = gcode;
704  }
705 
706  if (current_minimum < optimization_tolerance_loc) {
707  break;
708  }
709  }
710  }
711 
712  // If OSR search did not find a fully disentangling solution, keep the
713  // best prefix candidates discovered so far and evaluate them with
714  // Hilbert-Schmidt optimization below.
715  if (use_osr && all_solutions.empty() && !ci.prefixes.empty()) {
716  all_solutions.reserve(ci.prefixes.size());
717  for (std::map<GrayCodeCNOT, SearchNode>::const_iterator it = ci.prefixes.begin(); it != ci.prefixes.end(); ++it) {
718  all_solutions.emplace_back(it->first.copy());
719  }
720 
721  std::stringstream sstream;
722  sstream << "OSR did not find a fully disentangled solution; evaluating best prefix candidates with Hilbert-Schmidt optimization." << std::endl;
723  print(sstream, 1);
724  }
725  }
726  if (use_osr || use_graph_search) {
727  N_Qubit_Decomposition_custom&& cDecomp_custom_random = perform_optimization(nullptr);
728  std::uniform_real_distribution<> distrib_real(0.0, 2 * M_PI);
729  std::vector<double> optimized_parameters;
730  current_minimum = std::numeric_limits<double>::max();
731  if (all_solutions.size() == 0) {
732  // Last-resort fallback: evaluate the current best-known structure.
733  all_solutions.emplace_back(best_solution.copy());
734  }
735  for (const GrayCodeCNOT& solution : all_solutions) {
736  std::unique_ptr<Gates_block> gate_structure_loc;
737  gate_structure_loc.reset(construct_gate_structure_from_Gray_code(solution));
738  cDecomp_custom_random.set_custom_gate_structure(gate_structure_loc.get());
739  cDecomp_custom_random.set_optimization_blocks(gate_structure_loc->get_gate_num());
740 
741  // ----------- start the decomposition -----------
742  double current_minimum_tmp;
743  for (int iter = 0; iter < 5; iter++) {
744  optimized_parameters.resize(cDecomp_custom_random.get_parameter_num());
745  for (size_t idx = 0; idx < optimized_parameters.size(); idx++) {
746  optimized_parameters[idx] = distrib_real(gen);
747  }
748  cDecomp_custom_random.set_optimized_parameters(optimized_parameters.data(),
749  static_cast<int>(optimized_parameters.size()));
750  cDecomp_custom_random.start_decomposition();
751  current_minimum_tmp = cDecomp_custom_random.get_current_minimum();
752  if (current_minimum_tmp < optimization_tolerance_loc) {
753  break;
754  }
755  }
756  if (current_minimum_tmp < current_minimum) {
757  current_minimum = current_minimum_tmp;
758  optimized_parameters_mtx = cDecomp_custom_random.get_optimized_parameters().copy();
760  best_solution = solution;
761  }
762  if (current_minimum < optimization_tolerance_loc && stop_first_solution) {
763  break;
764  }
765  }
766  }
767 
768  if (current_minimum > optimization_tolerance_loc) {
769  std::stringstream sstream;
770  sstream << "Decomposition did not reach prescribed high numerical precision." << std::endl;
771  print(sstream, 1);
772  }
773 
774  return construct_gate_structure_from_Gray_code(best_solution);
775 }
776 
778  N_Qubit_Decomposition_custom& cDecomp_custom_random, MinCnotBoundSolver& osr_bound_solver,
779  std::vector<std::vector<int>>& all_cuts, double Fnorm, double osr_tol,
780  std::uniform_real_distribution<>& distrib_real, std::mt19937& gen,
781  const GrayCodeCNOT& path) {
782  SearchNode ev_results(path);
783  std::unique_ptr<Gates_block> gate_structure_loc(
785  cDecomp_custom_random.set_custom_gate_structure(gate_structure_loc.get());
786  cDecomp_custom_random.set_optimization_blocks(gate_structure_loc->get_gate_num());
787  std::vector<double> optimized_parameters(cDecomp_custom_random.get_parameter_num());
788  for (size_t idx = 0; idx < optimized_parameters.size(); idx++) {
789  optimized_parameters[idx] = distrib_real(gen);
790  }
791  cDecomp_custom_random.set_optimized_parameters(optimized_parameters.data(),
792  static_cast<int>(optimized_parameters.size()));
793  Matrix U;
794  Matrix_float U_float;
795  Matrix_real_float params_float;
796  for (const std::vector<int>& cut : all_cuts) {
797  if (cut.size() != 1) continue;
798  int max_rank = 2*(int)std::min(cut.size(), qbit_num-cut.size());
799  //int max_rank = 2;
800  std::tuple<int, double, std::vector<int>, std::vector<std::pair<int, double>>> rank_result;
801  for (int rank = max_rank-1; rank >= 0; rank--) {
802  cDecomp_custom_random.set_osr_params({cut}, rank, false);
803  //cDecomp_custom_random.set_osr_params(all_cuts, rank, true);
804  cDecomp_custom_random.start_decomposition();
805  Matrix_real params = cDecomp_custom_random.get_optimized_parameters();
806  if ( use_float ) {
807  params.copy_to(params_float);
808  Umtx_float.copy_to(U_float);
809  cDecomp_custom_random.apply_to(params_float, U_float);
810  }
811  else {
812  Umtx.copy_to(U);
813  cDecomp_custom_random.apply_to(params, U);
814  }
815  std::vector<std::pair<int, double>> osr_result;
816  osr_result.reserve(all_cuts.size());
817  int newrank = rank;
818  for (const std::vector<int>& eval_cut : all_cuts) {
819  if ( use_float ) {
820  osr_result.emplace_back(operator_schmidt_rank(U_float, qbit_num, eval_cut, Fnorm, osr_tol));
821  }
822  else {
823  osr_result.emplace_back(operator_schmidt_rank(U, qbit_num, eval_cut, Fnorm, osr_tol));
824  }
825  if (cut == eval_cut) newrank = osr_result.back().first;
826  //newrank = std::max(newrank, osr_result.back().first);
827  }
828  double best_kappa = std::numeric_limits<double>::infinity();
829  std::vector<int> best_edge_counts;
830  int min_cnots = osr_bound_solver.solve_min_cnots(osr_result, best_kappa, best_edge_counts);
831  if (newrank <= rank || rank == max_rank-1)
832  rank_result = std::make_tuple(min_cnots, best_kappa, std::move(best_edge_counts), std::move(osr_result));
833  if (newrank > rank) break;
834  rank = std::min(rank, newrank);
835  }
836  ev_results.osr_results.emplace_back(std::move(rank_result));
837  //if (ev_results.size() == (all_cuts.size()+1)/2) break;
838  }
839  return ev_results;
840 };
841 
842 std::vector<uint32_t> build_pred_mask(const GrayCodeCNOT& ops,
843  const std::vector<matrix_base<int>>& topology) {
844  const int m = static_cast<int>(ops.size());
845  std::vector<uint32_t> pred_mask(m, 0);
846 
847  std::unordered_map<int,int> last_on;
848  last_on.reserve(m * 2);
849 
850  for (int k = 0; k < m; ++k) {
851  int a = topology[ops[k]][0];
852  int b = topology[ops[k]][1];
853 
854  for (int q : {a, b}) {
855  std::unordered_map<int,int>::iterator it = last_on.find(q);
856  if (it != last_on.end()) {
857  int prev = it->second;
858  pred_mask[k] |= (1u << prev);
859  it->second = k;
860  } else {
861  last_on.emplace(q, k);
862  }
863  }
864  }
865 
866  return pred_mask;
867 }
868 
870  const GrayCodeCNOT& smallpath, const GrayCodeCNOT& bigpath,
871  const std::vector<matrix_base<int>>& topology)
872 {
873  std::vector<uint32_t> pred_mask = build_pred_mask(smallpath, topology);
874  const int m = static_cast<int>(smallpath.size());
875  if (m == 0) return true;
876  if (m > 31) {
877  // this should never happen
878  throw std::runtime_error("pattern too large for uint32_t mask");
879  }
880 
881  const uint32_t FULL = (1u << m) - 1u;
882 
883  // reachable[S] = whether subset S of small nodes can be matched
884  // after scanning some prefix of big
885  std::vector<char> reachable(size_t(1) << m, 0), next_reachable(size_t(1) << m, 0);
886  reachable[0] = 1;
887 
888  for (int i = 0; i < bigpath.size(); i++) {
889  int b = bigpath[i];
890  next_reachable = reachable; // skipping b is always allowed
891 
892  for (uint32_t S = 0; S <= FULL; ++S) {
893  if (!reachable[S]) continue;
894 
895  // try matching b to any currently available node u
896  for (int u = 0; u < m; ++u) {
897  uint32_t bit = 1u << u;
898  if (S & bit) continue; // already matched
899 
900  // all predecessors of u must already be in S
901  if ((pred_mask[u] & ~S) != 0) continue;
902 
903  // labels must match
904  if (smallpath[u] != b) continue;
905 
906  next_reachable[S | bit] = 1;
907  }
908  }
909 
910  reachable.swap(next_reachable);
911 
912  if (reachable[FULL]) return true;
913  }
914 
915  return reachable[FULL];
916 }
917 
919  std::vector<GrayCodeCNOT> patterns;
920  const std::vector<matrix_base<int>>& topology;
921 
922  ForbiddenSubseqSet(const std::vector<matrix_base<int>>& topology) : topology(topology) {}
923 
924  // Returns true if candidate should be pruned
925  bool contains_forbidden_subsequence(const GrayCodeCNOT& candidate) const {
926  for (const GrayCodeCNOT& pat : patterns) {
927  if (contains_topological_subsequence(pat, candidate, topology)) {
928  return true;
929  }
930  }
931  return false;
932  }
933 
934  // Insert a newly discovered forbidden path, keeping only minimal patterns
935  void insert_forbidden(const GrayCodeCNOT& path) {
936  // If already covered by a smaller forbidden pattern, skip
937  for (const GrayCodeCNOT& pat : patterns) {
938  if (contains_topological_subsequence(pat, path, topology)) {
939  return;
940  }
941  }
942 
943  // Remove any existing patterns that are supersets of the new one
944  patterns.erase(
945  std::remove_if(
946  patterns.begin(), patterns.end(),
947  [&](const GrayCodeCNOT& pat) {
948  return contains_topological_subsequence(pat, path, topology);
949  }),
950  patterns.end()
951  );
952 
953  patterns.push_back(path);
954  }
955 };
956 
958  std::vector<std::vector<int>> all_cuts = unique_cuts(qbit_num);
959  std::sort(all_cuts.begin(), all_cuts.end(), [](const std::vector<int>& a, const std::vector<int>& b){
960  if (a.size() != b.size()) return a.size() < b.size();
961  return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
962  });
963  // If topology entries are actual gates, the path stores topology indices.
964  double Fnorm = std::sqrt(static_cast<double>(1 << qbit_num));
965  double osr_tol = 1e-3;
966  MinCnotBoundSolver osr_bound_solver(qbit_num, all_cuts, topology);
967  //std::priority_queue<SearchNode, std::vector<SearchNode>, std::greater<SearchNode>> heap;
968  std::unique_ptr<SearchNode> top_heap;
969  std::set<GrayCodeCNOT> visited;
970  //ForbiddenSubseqSet forbidden(topology);
971 
972  N_Qubit_Decomposition_custom&& cDecomp_custom_random = perform_optimization(nullptr);
973  cDecomp_custom_random.set_cost_function_variant(OSR_ENTANGLEMENT);
974  std::uniform_real_distribution<> distrib_real(0.0, 2 * M_PI);
975 
976  std::function<bool(const GrayCodeCNOT&)> add_to_heap = [&](const GrayCodeCNOT& path) -> bool {
977  if (!is_unique_structure(path, topology))
978  return false; // not unique structure
979 
980  bool inserted = visited.insert(path).second;
981 
982  if (!inserted) {
983  return false;
984  }
985  // if (forbidden.contains_forbidden_subsequence(path)) {
986  // return false;
987  // }
988  // for (int i = 0; i < path.size(); i++) {
989  // if (visited.find(path.remove_Digit(i)) == visited.end()) {
990  // return false;
991  // }
992  // }
993 
994  //std::chrono::time_point<std::chrono::high_resolution_clock> start = std::chrono::high_resolution_clock::now();
995  SearchNode sn = evaluate_path(cDecomp_custom_random, osr_bound_solver, all_cuts, Fnorm, osr_tol, distrib_real, gen, path);
996  //printf("%.2fs\n", std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count()*1e-9);
997  // if (path.size()+sn.get_min_cnots() > level_limit) {
998  // forbidden.insert_forbidden(path);
999  // return false;
1000  // }
1001 
1002  if (top_heap == nullptr || !(*top_heap < sn)) {
1003  top_heap.reset(new SearchNode(std::move(sn)));
1004  }
1005  // heap.emplace(sn);
1006  return true;
1007  };
1008 
1009  GrayCodeCNOT startpath;
1010  if (qbit_num > 1)
1011  add_to_heap(startpath);
1012 
1013  std::vector<int> full_topo_filter(topology.size());
1014  std::iota(full_topo_filter.begin(), full_topo_filter.end(), 0);
1015 
1016  while (top_heap != nullptr) {
1017  std::unique_ptr<SearchNode> cur(top_heap.release());
1018  visited.clear(); // clear visited to save memory, relying on the fact that we won't revisit nodes anyway
1019  if (cur->get_min_cnots() == 0) {
1020  return cur->path;
1021  }
1022  const std::tuple<int, double, std::vector<int>, std::vector<std::pair<int, double>>>& cur_best_osr_result = cur->get_best_osr_result();
1023  const std::vector<int>& best_edge_counts = std::get<2>(cur_best_osr_result);
1024  std::vector<int> topo_filter;
1025  bool exact_edges = false;
1026  int num_cnot;
1027  if (!exact_edges) {
1028  num_cnot = 1;
1029  topo_filter.resize(topology.size());
1030  std::iota(topo_filter.begin(), topo_filter.end(), 0);
1031  std::sort(topo_filter.begin(), topo_filter.end(), [&](int a, int b){
1032  return best_edge_counts[a] > best_edge_counts[b];
1033  });
1034  } else {
1035  num_cnot = std::get<0>(cur_best_osr_result);
1036  topo_filter.reserve(std::get<0>(cur_best_osr_result));
1037  //topo_filter.resize(std::count_if(best_edge_counts.begin(), best_edge_counts.end(), [](int c){ return c > 0; }));
1038  for (size_t i = 0; i < best_edge_counts.size(); i++) {
1039  for (int j = 0; j < best_edge_counts[i]; j++) {
1040  topo_filter.push_back(static_cast<int>(i));
1041  }
1042  }
1043  }
1044 
1045  while (true) {
1046  // safety guard
1047  if (cur->path.size() + num_cnot > level_limit) {
1048  return cur->path; // best solution found within level limit, return immediately
1049  }
1050 
1051  generate_insertions(cur->path, topology, topo_filter, num_cnot,
1052  [&](const GrayCodeCNOT& newpath) {
1053  if (add_to_heap(newpath)) {
1054  //return cur > heap.top();
1055  return top_heap->get_min_cnots() == 0;
1056  }
1057  return false;
1058  });
1059 
1060  //const std::tuple<int, double, std::vector<int>, std::vector<std::pair<int, double>>>& top_best_osr_result = top_heap->get_best_osr_result();
1061  if (*cur > *top_heap || num_cnot == std::get<0>(cur_best_osr_result)) {
1062  // if (std::get<0>(top_best_osr_result) < std::get<0>(cur_best_osr_result) ||
1063  // std::get<0>(top_best_osr_result) == std::get<0>(cur_best_osr_result) &&
1064  // std::get<1>(top_best_osr_result) + 1e-3 < std::get<1>(cur_best_osr_result)) {
1065  break;
1066  }
1067 
1068 
1069  ++num_cnot;
1070 
1071  }
1072 
1073  // Optional beam trimming:
1074  // if beam_width > 0 and heap.size() > beam_width, can rebuild a trimmed heap here.
1075  }
1076  //printf("failed\n");
1077  return startpath; // single qubit fall-through case
1078 }
1079 
1108  CutInfo& ci) {
1109 
1110  tbb::spin_mutex tree_search_mutex;
1111 
1112  std::vector<std::vector<int>>& all_cuts = ci.all_cuts;
1113  MinCnotBoundSolver& osr_bound_solver = ci.osr_bound_solver;
1114  std::map<GrayCodeCNOT, SearchNode>& prefixes = ci.prefixes;
1115 
1116  double optimization_tolerance_loc;
1117  if (config.count("optimization_tolerance") > 0) {
1118  config["optimization_tolerance"].get_property(optimization_tolerance_loc);
1119  } else {
1120  optimization_tolerance_loc = optimization_tolerance;
1121  }
1122  long long stop_first_solution = 1;
1123  if (config.count("stop_first_solution") > 0) {
1124  config["stop_first_solution"].get_property(stop_first_solution);
1125  }
1126  GrayCodeCNOT best_solution;
1127  volatile bool found_optimal_solution = false;
1128 
1129  LevelResult level_result = level_num == 0 ? enumerate_unordered_cnot_BFS_level_init(qbit_num)
1131  const std::set<std::vector<int>>& visited = level_result.visited;
1132  const std::map<std::vector<int>, GrayCodeCNOT>& seq_pairs_of = level_result.seq_pairs_of;
1133  const std::vector<std::pair<std::vector<int>, GrayCodeCNOT>>& out_res = level_result.out_res;
1134 
1135  std::set<GrayCodeCNOT> pairs_reduced;
1136  for (const std::pair<std::vector<int>, GrayCodeCNOT>& item : out_res) {
1137  pairs_reduced.insert(item.second);
1138  }
1139  std::vector<GrayCodeCNOT> all_pairs(pairs_reduced.begin(), pairs_reduced.end());
1140  std::set<SearchNode> all_osr_results;
1141  int64_t iteration_max = all_pairs.size();
1142  std::vector<GrayCodeCNOT> successful_solutions;
1143  double Fnorm = std::sqrt(static_cast<double>(1 << qbit_num));
1144  double osr_tol = 1e-3;
1145 
1146  // determine the concurrency of the calculation
1147  unsigned int nthreads = std::thread::hardware_concurrency();
1148  int64_t concurrency = (int64_t)nthreads;
1149  concurrency = concurrency < iteration_max ? concurrency : iteration_max;
1150  std::uniform_real_distribution<> distrib_real(0.0, 2 * M_PI);
1151 
1152  int parallel = get_parallel_configuration();
1153 
1154  auto process_job_range = [&](int64_t begin, int64_t end) {
1155  N_Qubit_Decomposition_custom&& cDecomp_custom_random = perform_optimization(nullptr);
1156  cDecomp_custom_random.set_cost_function_variant(OSR_ENTANGLEMENT);
1157  std::mt19937 ts_gen(std::random_device{}());
1158 
1159  for (int64_t job_idx = begin; job_idx < end; ++job_idx) {
1160 
1161  // for( int64_t job_idx=0; job_idx<concurrency; job_idx++ ) {
1162 
1163  // initial offset and upper boundary of the gray code counter
1164  int64_t work_batch = iteration_max / concurrency;
1165  int64_t initial_offset = job_idx * work_batch;
1166  int64_t offset_max = (job_idx + 1) * work_batch - 1;
1167 
1168  if (job_idx == concurrency - 1) {
1169  offset_max = iteration_max - 1;
1170  }
1171 
1172  // std::cout << initial_offset << " " << offset_max << " " << iteration_max << " " << work_batch << " "
1173  // << concurrency << std::endl;
1174 
1175  for (int64_t iter_idx = initial_offset; iter_idx < offset_max + 1; iter_idx++) {
1176  if (stop_first_solution && found_optimal_solution) {
1177  break;
1178  }
1179  const GrayCodeCNOT& solution = all_pairs[iter_idx];
1180 
1181  SearchNode sn = evaluate_path(cDecomp_custom_random, osr_bound_solver, all_cuts, Fnorm, osr_tol, distrib_real, ts_gen, solution);
1183  cDecomp_custom_random
1184  .get_num_iters()); // retrieve the number of iterations spent on optimization
1185 
1186  const std::tuple<int, double, std::vector<int>, std::vector<std::pair<int, double>>>& osr_result = sn.get_best_osr_result();
1187  bool isWorse = false;
1188  for (int idx = 0; idx < solution.size(); idx++) {
1189  const GrayCodeCNOT& prefix = solution.remove_Digit(idx);
1190  std::map<GrayCodeCNOT, SearchNode>::const_iterator prefix_it = prefixes.find(prefix);
1191  if (prefix_it == prefixes.end()) {
1192  isWorse = true;
1193  break;
1194  }
1195  //if (sn > *prefix_it)
1196  const std::tuple<int, double, std::vector<int>, std::vector<std::pair<int, double>>>& prefix_osr_result = prefix_it->second.get_best_osr_result();
1197  if (std::get<0>(osr_result) > std::get<0>(prefix_osr_result) ||
1198  (std::get<0>(osr_result) == std::get<0>(prefix_osr_result) &&
1199  std::get<1>(osr_result) + 1e-3 < std::get<1>(prefix_osr_result))) {
1200  isWorse = true;
1201  break;
1202  }
1203  }
1204  int cnot_lower_bound = std::get<0>(osr_result);
1205  if (cnot_lower_bound <= level_limit - level_num && !isWorse) {
1206  tbb::spin_mutex::scoped_lock tree_search_lock{tree_search_mutex};
1207  all_osr_results.emplace(std::move(sn));
1208  if (cnot_lower_bound == 0) {
1209  found_optimal_solution = true;
1210  successful_solutions.push_back(solution.copy());
1211  }
1212  }
1213 
1214  /*for( int gcode_idx=0; gcode_idx<solution.size(); gcode_idx++ ) {
1215  std::cout << solution[gcode_idx] << ", ";
1216  }
1217  std::cout << current_minimum << std::endl;*/
1218  }
1219  }
1220  };
1221 
1222  if (parallel == 0) {
1223  process_job_range(0, concurrency);
1224  }
1225  else {
1226  int64_t work_batch = 1;
1227  // std::cout << "levels " << level_num << std::endl;
1228  tbb::parallel_for(
1229  tbb::blocked_range<int64_t>((int64_t)0, concurrency, work_batch), [&](tbb::blocked_range<int64_t> r) {
1230  process_job_range(r.begin(), r.end());
1231  });
1232  }
1233 
1234  long long beam_width = all_osr_results.size();
1235  if (config.count("beam") > 0) {
1236  config["beam"].get_property(beam_width);
1237  if (beam_width <= 0) beam_width = all_osr_results.size();
1238  }
1239  beam_width = std::min<long long>(beam_width, all_osr_results.size());
1240  std::map<GrayCodeCNOT, SearchNode> nextprefixes;
1241  for (std::set<SearchNode>::iterator item = all_osr_results.begin(); item != all_osr_results.end() && beam_width > 0; ++item, --beam_width) {
1242  nextprefixes.emplace(item->path, std::move(*item));
1243  }
1244  std::vector<std::vector<int>> next_q;
1245  next_q.reserve(out_res.size());
1246  for (std::vector<std::pair<std::vector<int>, GrayCodeCNOT>>::const_reverse_iterator it = out_res.crbegin();
1247  it != out_res.crend(); ++it) {
1248  if (nextprefixes.find(it->second) == nextprefixes.end()) {
1249  continue;
1250  }
1251  next_q.push_back(it->first);
1252  }
1254  result.solutions = std::move(successful_solutions);
1255  result.level_info.visited = std::move(visited);
1256  result.level_info.seq_pairs_of = std::move(seq_pairs_of);
1257  result.level_info.q = std::move(next_q);
1258  result.prefixes = std::move(nextprefixes);
1259  return result;
1260 }
1261 
1269 
1270  tbb::spin_mutex tree_search_mutex;
1271 
1272  double optimization_tolerance_loc;
1273  if (config.count("optimization_tolerance") > 0) {
1274  config["optimization_tolerance"].get_property(optimization_tolerance_loc);
1275  } else {
1276  optimization_tolerance_loc = optimization_tolerance;
1277  }
1278 
1279  if (level_num == 0) {
1280 
1281  // empty Gray code describing a circuit without two-qubit gates
1282  GrayCodeCNOT gcode;
1283  Gates_block* gate_structure_loc = construct_gate_structure_from_Gray_code(gcode);
1284 
1285  std::stringstream sstream;
1286  sstream << "Starting optimization with " << gate_structure_loc->get_gate_num() << " decomposing layers."
1287  << std::endl;
1288  print(sstream, 1);
1289 
1290  N_Qubit_Decomposition_custom&& cDecomp_custom_random = perform_optimization(gate_structure_loc);
1291 
1293  cDecomp_custom_random.get_num_iters()); // retrieve the number of iterations spent on optimization
1294 
1295  double current_minimum_tmp = cDecomp_custom_random.get_current_minimum();
1296  sstream.str("");
1297  sstream << "Optimization with " << level_num << " levels converged to " << current_minimum_tmp;
1298  print(sstream, 1);
1299 
1300  if (current_minimum_tmp < current_minimum) {
1301  current_minimum = current_minimum_tmp;
1302  optimized_parameters_mtx = cDecomp_custom_random.get_optimized_parameters();
1304  }
1305 
1306  // std::cout << "iiiiiiiiiiiiiiiiii " << current_minimum_tmp << std::endl;
1307  delete (gate_structure_loc);
1308  return gcode;
1309  }
1310 
1311  GrayCodeCNOT gcode_best_solution;
1312  bool found_optimal_solution = false;
1313 
1314  // set the limits for the N-ary Gray counter
1315 
1316  int n_ary_limit_max = static_cast<int>(topology.size());
1317  matrix_base<int8_t> n_ary_limits_int8(1, level_num); // array containing the limits of the individual Gray code elements
1318  memset(n_ary_limits_int8.get_data(), n_ary_limit_max, n_ary_limits_int8.size() * sizeof(int8_t));
1319  matrix_base<int> n_ary_limits(1, level_num); // array containing the limits of the individual Gray code elements
1320  memset(n_ary_limits.get_data(), n_ary_limit_max, n_ary_limits.size() * sizeof(int));
1321 
1322  for (int idx = 0; idx < n_ary_limits.size(); idx++) {
1323  n_ary_limits[idx] = n_ary_limit_max;
1324  n_ary_limits_int8[idx] = n_ary_limit_max;
1325  }
1326 
1327  int64_t iteration_max =
1328  static_cast<int64_t>(pow(static_cast<double>(n_ary_limit_max), static_cast<double>(level_num)));
1329 
1330  // determine the concurrency of the calculation
1331  unsigned int nthreads = std::thread::hardware_concurrency();
1332  int64_t concurrency = (int64_t)nthreads;
1333  concurrency = concurrency < iteration_max ? concurrency : iteration_max;
1334 
1335  int parallel = get_parallel_configuration();
1336 
1337  auto process_job_range = [&](int64_t begin, int64_t end) {
1338  for (int64_t job_idx = begin; job_idx < end; ++job_idx) {
1339 
1340  // for( int64_t job_idx=0; job_idx<concurrency; job_idx++ ) {
1341 
1342  // initial offset and upper boundary of the gray code counter
1343  int64_t work_batch = iteration_max / concurrency;
1344  int64_t initial_offset = job_idx * work_batch;
1345  int64_t offset_max = (job_idx + 1) * work_batch - 1;
1346 
1347  if (job_idx == concurrency - 1) {
1348  offset_max = iteration_max - 1;
1349  }
1350 
1351  // std::cout << initial_offset << " " << offset_max << " " << iteration_max << " " << work_batch << " "
1352  // << concurrency << std::endl;
1353 
1354  n_aryGrayCodeCounter gcode_counter(
1355  n_ary_limits, initial_offset); // see piquassoboost for details of the implementation
1356  gcode_counter.set_offset_max(offset_max);
1357  GrayCodeCNOT gcode(n_ary_limits_int8);
1358 
1359  for (int64_t iter_idx = initial_offset; iter_idx < offset_max + 1; iter_idx++) {
1360 
1361  if (found_optimal_solution) {
1362  return;
1363  }
1364 
1365  GrayCode&& gcodeint = gcode_counter.get();
1366  std::transform(gcodeint.data, gcodeint.data + gcodeint.size(), gcode.data,
1367  [](int val) { return static_cast<int8_t>(val); });
1368 
1369  if (!is_unique_structure(gcode, topology)) continue;
1370 
1371  Gates_block* gate_structure_loc = construct_gate_structure_from_Gray_code(gcode);
1372 
1373  // ----------- start the decomposition -----------
1374 
1375  std::stringstream sstream;
1376  sstream << "Starting optimization with " << gate_structure_loc->get_gate_num()
1377  << " decomposing layers." << std::endl;
1378  print(sstream, 1);
1379 
1380  N_Qubit_Decomposition_custom&& cDecomp_custom_random = perform_optimization(gate_structure_loc);
1381 
1382  delete (gate_structure_loc);
1383  gate_structure_loc = NULL;
1384 
1385  increment_num_iters(cDecomp_custom_random
1386  .get_num_iters()); // retrieve the number of iterations spent on optimization
1387 
1388  double current_minimum_tmp = cDecomp_custom_random.get_current_minimum();
1389  sstream.str("");
1390  sstream << "Optimization with " << level_num << " levels converged to " << current_minimum_tmp;
1391  print(sstream, 1);
1392 
1393  // std::cout << "Optimization with " << level_num << " levels converged to " << current_minimum_tmp
1394  // << std::endl;
1395 
1396  {
1397  tbb::spin_mutex::scoped_lock tree_search_lock{tree_search_mutex};
1398 
1399  if (current_minimum_tmp < current_minimum && !found_optimal_solution) {
1400 
1401  current_minimum = current_minimum_tmp;
1402  gcode_best_solution = gcode;
1403 
1404  optimized_parameters_mtx = cDecomp_custom_random.get_optimized_parameters();
1406  }
1407 
1408  if (current_minimum < optimization_tolerance_loc && !found_optimal_solution) {
1409  found_optimal_solution = true;
1410  }
1411  }
1412 
1413  /*
1414  for( int gcode_idx=0; gcode_idx<gcode.size(); gcode_idx++ ) {
1415  std::cout << gcode[gcode_idx] << ", ";
1416  }
1417  std::cout << current_minimum_tmp << std::endl;
1418  */
1419 
1420  // iterate the Gray code to the next element
1421  int changed_index, value_prev, value;
1422  if (gcode_counter.next(changed_index, value_prev, value)) {
1423  // exit from the for loop if no further gcode is present
1424  break;
1425  }
1426  }
1427  }
1428  };
1429 
1430  if (parallel == 0) {
1431  process_job_range(0, concurrency);
1432  }
1433  else {
1434  int64_t work_batch = 1;
1435  // std::cout << "levels " << level_num << std::endl;
1436  tbb::parallel_for(
1437  tbb::blocked_range<int64_t>((int64_t)0, concurrency, work_batch), [&](tbb::blocked_range<int64_t> r) {
1438  process_job_range(r.begin(), r.end());
1439  });
1440  }
1441 
1442  return gcode_best_solution;
1443 }
1444 
1451 
1452  double optimization_tolerance_loc;
1453  if (config.count("optimization_tolerance") > 0) {
1454  config["optimization_tolerance"].get_property(optimization_tolerance_loc);
1455  } else {
1456  optimization_tolerance_loc = optimization_tolerance;
1457  }
1458 
1459  N_Qubit_Decomposition_custom cDecomp_custom_random;
1460  if ( use_float ) {
1461  cDecomp_custom_random =
1463  }
1464  else {
1465  cDecomp_custom_random =
1467  }
1468  if (gate_structure_loc != nullptr) {
1469  cDecomp_custom_random.set_custom_gate_structure(gate_structure_loc);
1470  cDecomp_custom_random.set_optimization_blocks(gate_structure_loc->get_gate_num());
1471  }
1472  cDecomp_custom_random.set_max_iteration(max_outer_iterations);
1473 #ifndef __DFE__
1474  cDecomp_custom_random.set_verbose(verbose);
1475 #else
1476  cDecomp_custom_random.set_verbose(0);
1477 #endif
1478  cDecomp_custom_random.set_cost_function_variant(cost_fnc);
1479  cDecomp_custom_random.set_debugfile("");
1480  cDecomp_custom_random.set_optimization_tolerance(optimization_tolerance_loc);
1481  cDecomp_custom_random.set_trace_offset(trace_offset);
1482  cDecomp_custom_random.set_optimizer(alg);
1483  cDecomp_custom_random.set_project_name(project_name);
1484  if (alg == ADAM || alg == BFGS2) {
1485  int max_inner_iterations_loc = 10000;
1486  if (gate_structure_loc != nullptr) {
1487  int param_num_loc = gate_structure_loc->get_parameter_num();
1488  max_inner_iterations_loc = static_cast<int>((double)param_num_loc / 852 * 10000000.0);
1489  }
1490  cDecomp_custom_random.set_max_inner_iterations(max_inner_iterations_loc);
1491  cDecomp_custom_random.set_random_shift_count_max(5);
1492  } else if (alg == ADAM_BATCHED) {
1493  cDecomp_custom_random.set_optimizer(alg);
1494  int max_inner_iterations_loc = 2000;
1495  cDecomp_custom_random.set_max_inner_iterations(max_inner_iterations_loc);
1496  cDecomp_custom_random.set_random_shift_count_max(5);
1497  } else if (alg == BFGS) {
1498  cDecomp_custom_random.set_optimizer(alg);
1499  int max_inner_iterations_loc = 10000;
1500  cDecomp_custom_random.set_max_inner_iterations(max_inner_iterations_loc);
1501  }
1502 
1503  if (gate_structure_loc != nullptr)
1504  cDecomp_custom_random.start_decomposition();
1505  return cDecomp_custom_random;
1506 }
1507 
1516  bool finalize) {
1517 
1518  // determine the target qubit indices and control qbit indices for the CNOT gates from the Gray code counter
1519  matrix_base<int> target_qbits(1, gcode.size());
1520  matrix_base<int> control_qbits(1, gcode.size());
1521 
1522  for (int gcode_idx = 0; gcode_idx < gcode.size(); gcode_idx++) {
1523 
1524  int target_qbit = possible_target_qbits[gcode[gcode_idx]];
1525  int control_qbit = possible_control_qbits[gcode[gcode_idx]];
1526 
1527  target_qbits[gcode_idx] = target_qbit;
1528  control_qbits[gcode_idx] = control_qbit;
1529 
1530  // std::cout << target_qbit << " " << control_qbit << std::endl;
1531  }
1532 
1533  // ----------- contruct the gate structure to be optimized -----------
1534  Gates_block* gate_structure_loc = new Gates_block(qbit_num);
1535 
1536  for (int gcode_idx = 0; gcode_idx < gcode.size(); gcode_idx++) {
1537 
1538  // add new 2-qbit block to the circuit
1539  add_two_qubit_block(gate_structure_loc, target_qbits[gcode_idx], control_qbits[gcode_idx]);
1540  }
1541 
1542  // add finalizing layer to the gate structure
1543  if (finalize)
1544  add_finalyzing_layer(gate_structure_loc);
1545 
1546  return gate_structure_loc;
1547 }
1548 
1556  int control_qbit) {
1557 
1558  if (control_qbit >= qbit_num || target_qbit >= qbit_num) {
1559  std::string error("N_Qubit_Decomposition_Tree_Search::add_two_qubit_block: Label of control/target qubit "
1560  "should be less than the number of qubits in the register.");
1561  throw error;
1562  }
1563 
1564  if (control_qbit == target_qbit) {
1565  std::string error(
1566  "N_Qubit_Decomposition_Tree_Search::add_two_qubit_block: Target and control qubits should be different");
1567  throw error;
1568  }
1569 
1570  Gates_block* layer = new Gates_block(qbit_num);
1571  /*layer->add_rz(target_qbit);
1572  layer->add_ry(target_qbit);
1573  layer->add_rz(target_qbit);
1574 
1575  layer->add_rz(control_qbit);
1576  layer->add_ry(control_qbit);
1577  layer->add_rz(control_qbit);*/
1578 
1579  layer->add_u3(target_qbit);
1580  layer->add_u3(control_qbit);
1581  layer->add_cnot(target_qbit, control_qbit);
1582  gate_structure->add_gate(layer);
1583 }
1584 
1590 
1591  // creating block of gates
1592  Gates_block* block = new Gates_block(qbit_num);
1593  /*
1594  block->add_un();
1595  block->add_ry(qbit_num-1);
1596  */
1597  for (int idx = 0; idx < qbit_num; idx++) {
1598  // block->add_rz(idx);
1599  // block->add_ry(idx);
1600  // block->add_rz(idx);
1601  block->add_u3(idx);
1602  // block->add_u3(idx, Theta, Phi, Lambda);
1603  // block->add_ry(idx);
1604  }
1605 
1606  // adding the operation block to the gates
1607  if (gate_structure == NULL) {
1608  throw("N_Qubit_Decomposition_Tree_Search::add_finalyzing_layer: gate_structure is null pointer");
1609  } else {
1610  gate_structure->add_gate(block);
1611  }
1612 }
1613 
1619 
1620  Umtx = Umtx_new;
1621  if ( use_float ) {
1622  Umtx_float = Umtx_new.to_float32();
1623  }
1624 }
1625 
1627 
1628  Umtx_float = Umtx_new;
1629  Umtx = Umtx_new.to_float64();
1630  use_float = true;
1631 }
optimization_aglorithms alg
The optimization algorithm to be used in the optimization.
void set_osr_params(std::vector< std::vector< int >> use_cuts_in, int osr_rank_in, bool use_softmax_in)
void print(const std::stringstream &sstream, int verbose_level=1) const
Call to print output messages in the function of the verbosity level.
Definition: logging.cpp:55
std::vector< std::tuple< int, double, std::vector< int >, std::vector< std::pair< int, double > > > > osr_results
Class to store single-precision real arrays and properties.
virtual Gates_block * determine_gate_structure(Matrix_real &optimized_parameters_mtx)
Call determine the gate structrue of the decomposing circuit.
Matrix_float to_float32() const
Convert to single precision.
Definition: matrix.cpp:32
std::map< GrayCodeCNOT, SearchNode > prefixes
Map from Gray code sequences to their OSR result pairs (rank, cost) for different cuts...
std::vector< std::vector< int > > unique_cuts(int n)
MinCnotBoundSolver osr_bound_solver
Map from CNOT pair (target, control) to the indices of cuts that are affected by this pair...
int get_num_iters()
Get the number of processed iterations during the optimization process.
void set_optimizer(optimization_aglorithms alg_in)
Call to set the optimizer engine to be used in solving the optimization problem.
Definition: S.h:11
void set_project_name(std::string &project_name_new)
Call to set the name of the project.
void insert_forbidden(const GrayCodeCNOT &path)
void add_two_qubit_block(Gates_block *gate_structure, int target_qbit, int control_qbit)
Call to add two-qubit building block (two single qubit rotation blocks and one two-qubit gate) to the...
void set_custom_gate_structure(Gates_block *gate_structure_in)
Call to set custom layers to the gate structure that are intended to be used in the subdecomposition...
std::vector< int > target_qbits
Vector of target qubit indices (for multi-qubit gates)
Definition: Gate.h:102
Matrix_real copy() const
Call to create a copy of the matrix.
int control_qbit
The index of the qubit which acts as a control qubit (control_qbit >= 0) in controlled operations...
Definition: Gate.h:100
std::set< std::vector< int > > visited
Set of visited states (represented as vectors of integers)
void add_gate(Gate *gate)
Append a general gate to the list of gates.
Matrix to_float64() const
Convert to double precision.
Definition: matrix_float.cpp:8
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
std::vector< std::pair< std::vector< int >, GrayCodeCNOT > > out_res
Vector of output results (discoveries) from the BFS level enumeration.
static int is_unique_structure(const GrayCodeCNOT &path, const std::vector< matrix_base< int >> &topology)
bool use_float
Selects float32 circuit application for parameter/unitary/state data.
cost_function_type cost_fnc
The chosen variant of the cost function.
N_Qubit_Decomposition_Tree_Search()
Nullary constructor of the class.
matrix_base< int > possible_target_qbits
List of possible target qubits according to the topology – paired up with possible control qubits...
GrayCodeCNOT tree_search_over_gate_structures(int level_num)
Call to perform tree search over possible gate structures.
std::vector< std::vector< int > > q
Queue of states to be processed in the next BFS level.
int target_qbit
The index of the qubit on which the operation acts (target_qbit >= 0)
Definition: Gate.h:98
scalar * data
pointer to the stored data
Definition: matrix_base.hpp:48
double get_current_minimum()
Call to get the obtained minimum of the cost function.
void release_gates()
Call to release the stored gates.
const std::vector< matrix_base< int > > & topology
void set_trace_offset(int trace_offset_in)
Set the trace offset used in the evaluation of the cost function.
int trace_offset
The offset in the first columns from which the "trace" is calculated. In this case Tr(A) = sum_(i-off...
Copyright 2021 Budapest Quantum Computing Group.
std::map< GrayCodeCNOT, SearchNode > prefixes
Map of GrayCodeCNOT to OSR (Operator Schmidt Rank) result pairs.
void add_cnot(int target_qbit, int control_qbit)
Append a CNOT gate gate to the list of gates.
void set_random_shift_count_max(int random_shift_count_max_in)
Call to set the maximal number of parameter randomization tries to escape a local minimum...
void increment_num_iters(int delta=1)
Atomically increment the tracked number of optimization iterations.
scalar * get_data() const
Call to get the pointer to the stored data.
void set_offset_max(const int64_t &value)
std::vector< uint32_t > build_pred_mask(const GrayCodeCNOT &ops, const std::vector< matrix_base< int >> &topology)
N_Qubit_Decomposition_custom perform_optimization(Gates_block *gate_structure_loc)
Call to perform the optimization on the given gate structure.
int level_limit
The maximal number of adaptive layers used in the decomposition.
int next()
Iterate the counter to the next value.
void sync_optimized_parameters_float()
Synchronize the float32 parameter mirror from the double optimizer storage.
int get_gate_num()
Call to get the number of gates grouped in the class.
static int canonical_prefix_ok(const GrayCodeCNOT &path, const std::vector< matrix_base< int >> &topology)
std::vector< std::pair< std::vector< int >, GrayCodeCNOT > > Discovery
std::vector< std::vector< int > > all_cuts
Vector of all possible qubit cuts, where each cut is represented as a vector of qubit indices...
int max_outer_iterations
Maximal number of iterations allowed in the optimization process.
std::string project_name
the name of the project
void set_max_inner_iterations(int max_inner_iterations_in)
Call to set the maximal number of iterations for which an optimization engine tries to solve the opti...
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
double optimization_tolerance
The maximal allowed error of the optimization problem (The error of the decomposition would scale wit...
GrayCode_base remove_Digit(const int idx) const
Call to add a new digit to the Gray code.
int accelerator_num
number of utilized accelerators
std::vector< matrix_base< int > > topology
A vector of index pairs encoding the connectivity between the qubits.
matrix_base< int > possible_control_qbits
List of possible control qubits according to the topology – paired up with possible target qubits...
std::pair< int, double > operator_schmidt_rank(const Matrix &U, int n, const std::vector< int > &A_qubits, double Fnorm, double tol=1e-10)
void set_debugfile(std::string debugfile)
Call to set the debugfile name.
Definition: logging.cpp:95
static LevelResult enumerate_unordered_cnot_BFS_level_init(int n)
Initialize the breadth-first search (BFS) enumeration at depth 0 (identity state only).
Matrix_float Umtx_float
Float32 copy of the unitary used when config["use_float"] is true.
#define M_PI
Definition: qgd_math.h:42
Matrix_real get_optimized_parameters()
Call to get the optimized parameters.
virtual void add_finalyzing_layer()
Call to add further layer to the gate structure used in the subdecomposition.
Structure containing the result of tree search over gate structures with Gray code and level informat...
void add_u3(int target_qbit)
Append a U3 gate to the list of gates.
std::vector< GrayCodeCNOT > patterns
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
LevelInfo level_info
Updated LevelInfo containing visited states and sequence pairs.
Header file for a class implementing the adaptive gate decomposition algorithm of arXiv:2203...
void combine(Gates_block *op_block)
Call to append the gates of an gate block to the current block.
GrayCode_base add_Digit(const intType n_ary_limit) const
Call to add a new digit to the Gray code.
std::set< std::vector< int > > visited
Set of visited states (represented as vectors of integers)
Structure containing level information for breadth-first search over gate structures.
void set_optimized_parameters(double *parameters, int num_of_parameters)
Call to set the optimized parameters for initial optimization.
std::map< std::vector< int >, GrayCodeCNOT > seq_pairs_of
Map from state vectors to their corresponding Gray code sequences.
void generate_insertions(const GrayCodeCNOT &curpath, const std::vector< matrix_base< int >> &topology, const std::vector< int > &topo_filt, int num_cnot, Callback &&callback)
virtual void apply_to(Matrix_real &parameters_mtx, Matrix &input, int parallel=0) override
Call to apply the gate on the input array/matrix Gates_block*input.
int num_threads
Store the number of OpenMP threads. (During the calculations OpenMP multithreading is turned off...
SearchNode evaluate_path(N_Qubit_Decomposition_custom &cDecomp_custom_random, MinCnotBoundSolver &osr_bound_solver, std::vector< std::vector< int >> &all_cuts, double Fnorm, double osr_tol, std::uniform_real_distribution<> &distrib_real, std::mt19937 &gen, const GrayCodeCNOT &path)
int solve_min_cnots(const std::vector< std::pair< int, double > > &cut_bounds, int max_total=-1) const
void generate_insertions_recursive(const GrayCodeCNOT &curpath, const std::vector< matrix_base< int >> &topology, const std::vector< int > &topo_filt, int num_cnot, std::vector< int > &places, std::vector< int > &pairs, int depth, int min_place, Callback &&callback, bool &early_stop)
int verbose
Set the verbosity level of the output messages.
Definition: logging.h:50
Matrix copy() const
Call to create a copy of the matrix.
Definition: matrix.h:57
void set_optimization_tolerance(double tolerance_in)
Call to set the tolerance of the optimization processes.
void copy_to(Matrix_real &target) const
Copy the matrix to a reusable double-precision target matrix.
Double-precision complex matrix (float64).
Definition: matrix.h:38
void copy_to(matrix_base< scalar > &target) const
Copy the current matrix storage into a reusable target matrix.
TreeSearchResult tree_search_over_gate_structures_osr(int level_num, LevelInfo &li, CutInfo &ci)
Perform tree search over possible gate structures using Gray code enumeration and Operator Schmidt Ra...
int size() const
Call to get the number of the allocated elements.
std::vector< int > control_qbits
Vector of control qubit indices (for multi-qubit gates)
Definition: Gate.h:104
GrayCode_base copy() const
Call to create a copy of the state.
Gates_block()
Default constructor of the class.
Definition: Gates_block.cpp:82
A class responsible for grouping two-qubit (CNOT,CZ,CH) and one-qubit gates into layers.
Definition: Gates_block.h:44
void omp_set_num_threads(int num_threads)
Set the number of threads on runtime in MKL.
virtual void start_decomposition()
Start the disentanglig process of the unitary.
GrayCode get()
Get the current gray code counter value.
Single-precision complex matrix (float32).
Definition: matrix_float.h:41
void set_verbose(int verbose_in)
Call to set the verbose attribute.
Definition: logging.cpp:85
std::map< std::string, Config_Element > config
config metadata utilized during the optimization
Gates_block * construct_gate_structure_from_Gray_code(const GrayCodeCNOT &gcode, bool finalize=true)
Call to construct a gate structure corresponding to the configuration of the two-qubit gates describe...
bool contains_forbidden_subsequence(const GrayCodeCNOT &candidate) const
virtual void start_decomposition()
Start the disentanglig process of the unitary.
ForbiddenSubseqSet(const std::vector< matrix_base< int >> &topology)
std::vector< GrayCodeCNOT > solutions
Vector of successful Gray-code solutions.
void set_unitary(Matrix &Umtx_new)
Set unitary matrix.
const std::tuple< int, double, std::vector< int >, std::vector< std::pair< int, double > > > & get_best_osr_result() const
Header file for the paralleized calculation of the cost function of the final optimization problem (s...
static LevelResult enumerate_unordered_cnot_BFS_level_step(LevelInfo &L, const std::vector< matrix_base< int >> &topology, bool use_gl=true)
Perform one expansion level of breadth-first search (BFS) enumeration over CNOT gate structures...
volatile double current_minimum
The current minimum of the optimization problem.
Matrix Umtx
The unitary to be decomposed.
void export_gate_list_to_binary(Matrix_real &parameters, Gates_block *gates_block, const std::string &filename, int verbosity)
Use to export a quantum circuit into binary format.
Structure containing cut information for operator Schmidt rank (OSR) analysis.
void export_unitary(std::string &filename)
exports unitary matrix to binary file
bool contains_topological_subsequence(const GrayCodeCNOT &smallpath, const GrayCodeCNOT &bigpath, const std::vector< matrix_base< int >> &topology)
int qbit_num
number of qubits spanning the matrix of the operation
Definition: Gate.h:94
void set_max_iteration(int max_outer_iterations_in)
Call to set the maximal number of the iterations in the optimization process.
double decomposition_error
error of the final decomposition
int max_inner_iterations
the maximal number of iterations for which an optimization engine tries to solve the optimization pro...
Matrix_real optimized_parameters_mtx
The optimized parameters for the gates.
int get_parallel_configuration()
Get the parallel configuration from the config.
void set_cost_function_variant(cost_function_type variant)
Call to set the variant of the cost function used in the calculations.
int get_parameter_num() override
Call to get the number of free parameters.
Matrix_float copy() const
Call to create a copy of the matrix.
Definition: matrix_float.h:60
virtual ~N_Qubit_Decomposition_Tree_Search()
Destructor of the class.
Structure containing the result of a BFS level enumeration.
void set_optimization_blocks(int optimization_block_in)
Call to set the number of gate blocks to be optimized in one shot.
Class to store data of complex arrays and its properties.
Definition: matrix_real.h:41
std::map< std::vector< int >, GrayCodeCNOT > seq_pairs_of
Map from state vectors to their corresponding Gray code sequences.
std::mt19937 gen
Standard mersenne_twister_engine seeded with rd()
int omp_get_max_threads()
get the number of threads in MKL