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