diff --git a/documentation/source/general/changelog/0.4.rst b/documentation/source/general/changelog/0.4.rst index 6020fa18..cf3cea47 100644 --- a/documentation/source/general/changelog/0.4.rst +++ b/documentation/source/general/changelog/0.4.rst @@ -8,7 +8,7 @@ The latest update of Qrisp is (once again) the biggest (so far)! We integrated a Shor's Algorithm and modular arithmetic --------------------------------------- -With 0.4 we integrated the infrastructure to facility the implementation and compilation of Shor's algorithm. Most notably: +With 0.4 we integrated the infrastructure to facilitate the implementation and compilation of Shor's algorithm. Most notably: * The :ref:`QuantumModulus` quantum type, which allows you to utilize modular arithmetic in your algorithms with a minimal amount of knowledge of the `underlying circuits `_. * Furthermore, we implemented the :meth:`qcla ` introduced by `Wang et al `_. The previously mentioned arithmetic can be adapted to use this adder (or any other adder for that matter!). @@ -26,7 +26,7 @@ As we found out, implementations of Shor's algorithm that are able to return a Q .. image:: 04_shor_plot.svg -This demonstrates how powerful the Qrisp version is compared to other compilers/implementations. The presented values for the are averaged over several choices of $a$ per $N$. T-depth and T-count are computed under the (extremely optimistic) assumption that parametrized phase gates can be executed in unit time and unit cost. Without this assumption the Qrisp implementation brings a speed-up of almost 3 orders of magnitude! +This demonstrates how powerful the Qrisp version is compared to other compilers/implementations. The presented values are averaged over several choices of $a$ per $N$. T-depth and T-count are computed under the (extremely optimistic) assumption that parametrized phase gates can be executed in unit time and unit cost. Without this assumption the Qrisp implementation brings a speed-up of almost 3 orders of magnitude! Compiler upgrades @@ -37,7 +37,7 @@ The :meth:`compile ` function received two importa * Due to another topological ordering step, this function can now reduce the depth in many cases by a significant portion with minimal classical performance overhead. * It is now possible to specify the time each gate takes to optimize the overall run time in a physical execution of the quantum circuit. Read more about this feature :ref:`here `. This especially enables compilation for fault tolerant backends, as they are expected to be bottlenecked by T-gates. -This plot highlights how the Qrisp compiler evolved compared to the last version (and it's competitors). It shows the circuit depth (as acquired with :meth:`depth `) for the QAOA algorithm applied to a MaxCut problem. We benchmarked the code that is available as tutorial. +This plot highlights how the Qrisp compiler evolved compared to the last version (and its competitors). It shows the circuit depth (as acquired with :meth:`depth `) for the QAOA algorithm applied to a MaxCut problem. We benchmarked the code that is available as tutorial. .. image:: 04_compiler_plot.svg @@ -50,25 +50,25 @@ Algorithmic primitives We added the following algorithmic primitives to the Qrisp repertoire: * :meth:`amplitude_amplification ` is an algorithm, which allows you to boost the probability of measuring your desired solution. -* :meth:`QAE ` gives you an estimate of the amplitude of a certain sub-space without having to perform a possibly exponential amount of measurements. +* :meth:`QAE ` gives you an estimate of the probability of measuring your desired solution. * The ``gidney`` and ``jones`` methods for compiling :meth:`mcx ` gates with optimal T-depth in a fault-tolerant setting. * The :meth:`gidney_adder ` as documented `here `_. QUBO optimization ----------------- -QUBO is short for Quadratic Uncostrained Binary Optimization and a problem type, which captures a `large class of optimization problems `_. QUBO instances can now be :ref:`solved within the QAOA module `. +QUBO is short for Quadratic Unconstrained Binary Optimization and a problem type, which captures a `large class of optimization problems `_. QUBO instances can now be :ref:`solved within the QAOA module `. Simulator --------- -The Qrisp simulator received multiple powerfull performance upgrades such as a much faster sparse matrix multiplication algorithm and better statevector factoring. These upgrades facility the simulation of extremely large circuits (in some cases, we observed >200 qubits)! +The Qrisp simulator received multiple powerful performance upgrades such as a much faster sparse matrix multiplication algorithm and better statevector factoring. These upgrades facilitate the simulation of extremely large circuits (in some cases, we observed >200 qubits)! Network interface ----------------- -For remote backend queries, Qrisp now uses the network inteface developed in the `SequenC project `_. This project aims to build a uniform, open-source quantum cloud infrastructure. Note that specific backend vendors like IBMQuantum can still be called via :ref:`VirtualBackends `. +For remote backend queries, Qrisp now uses the network interface developed in the `SequenC project `_. This project aims to build a uniform, open-source quantum cloud infrastructure. Note that specific backend vendors like IBMQuantum can still be called via :ref:`VirtualBackends `. Docker Container ---------------- @@ -90,9 +90,9 @@ Bug-fixes --------- * Fixed a bug that caused false results in some simulations containing a Y-gate. -* Fixed a bug that prevented proper QFT cancelation within the :meth:`compile ` method in some cases. +* Fixed a bug that prevented proper QFT cancellation within the :meth:`compile ` method in some cases. * Fixed a bug that prevented proper verification of correct automatic uncomputation in some cases. * Fixed a bug that caused false determination of the unitary of controlled gates with a non-trivial control state. * Fixed a bug that caused problems during circuit visualisation on some platforms. -* Fixed a bug that caused the simulation progressbar to not vanish after the simulation concluded. +* Fixed a bug that caused the simulation progress bar to not vanish after the simulation concluded. * Fixed a bug that introduced an extra phase in the compilation of dirty-ancillae supported ``balauca`` MCX gates. diff --git a/documentation/source/general/tutorial/Sudoku.rst b/documentation/source/general/tutorial/Sudoku.rst new file mode 100644 index 00000000..b48e1eaf --- /dev/null +++ b/documentation/source/general/tutorial/Sudoku.rst @@ -0,0 +1,762 @@ +Solving Sudoku using Quantum Backtracking +========================================= +.. _sudoku: + +This tutorial is the practical hands on-part of `this paper `_. + +Sudoku +------ + +Sudoku is a popular logic-based puzzle game that gained widespread popularity in the late 20th century. Its name, "Sudoku," originates from the Japanese words "su" (meaning "number") and "doku" (meaning "single"). The puzzle consists of a grid typically composed of nine rows, nine columns, and nine smaller subgrids known as "regions" or "blocks." + +The objective of Sudoku is simple: fill in the grid so that each row, column, and region contains the numbers 1 through 9, with no repetition. A partially completed grid is provided, with some numbers already filled in. The challenge lies in using deductive reasoning and logic to determine the correct placement of numbers within the grid. + +Sudoku puzzles come in various difficulty levels, ranging from easy to extremely challenging, based on the number and placement of initial clues provided. While the rules remain consistent, the complexity of solving the puzzle increases with fewer initial clues and the necessity for more advanced solving strategies. + +Over the years, Sudoku has evolved into a beloved pastime for enthusiasts of all ages, offering a stimulating mental exercise that promotes concentration, critical thinking, and problem-solving skills. Whether played casually in newspapers, puzzle books, or digital platforms, Sudoku continues to captivate individuals worldwide with its timeless appeal. + +In our case, we will be working with 4x4 Sudoku mostly because we want to keep the results still simulable. A 9x9 or even 16x16 implementation would of course work equally well. + +| + +.. image:: ./sudoku.png + :width: 400 + :alt: Sudoku + :align: center + + +Backtracking +------------ + +As they fall into the category of constraint satisfaction problems, Sudokus are (in a quantum sense) usually tackled using Grover's algorithm (SPOILER ALERT: we won't be using that approach). In this case constructing the oracle is rather straight forward, if the circuits for evaluating numerical comparisons are available. This, however, comes with the drawback that the state space of the search grows exponentially, which the quadratic speed-up of the Grover search barely mitigates. Of course there are much better ways of solving a Sudoku than just trying out every single combination; the same also holds true for the quantum realm. In this tutorial you will learn how a strategy called *backtracking* can be used to utilize the problem structure to gain a performance advantage. + +Backtracking approaches encopass a large class of algorithms, which are usually specified by both an ``accept`` and ``reject`` function. Furthermore, a set of possible assignments to an array of fixed length is also required. For a more detailed introduction consider `this page `_ or `this page `_. In general, the algorithm in Python code usually boils down to: + +:: + + from problem import accept, reject, max_depth, assignments + + def backtracking(x): + + if accept(x): + return x + + if reject(x) or len(x) == max_depth: + return None + + for j in assigments: + y = list(x) + y.append(j) + res = backtracking(y) + if res is not None: + return res + +Quantum backtracking +-------------------- + +The quantum algorithm for solving backtracking problems has been +`proposed by Ashley Montanaro `_ and yields +a 1 to 1 correspondence between an arbitrary classical backtracking algorithm +and its quantum equivalent. The quantum version achieves a quadratic speed up +over the classical one. + +The algorithm is based on performing quantum phase estimation on a quantum walk +operator, which traverses the backtracking tree. The core algorithm returns +"Solution exists" if the 0 component of the quantum phase estimation result +has a higher probability then 3/8 = 0.375. + +Similar to the classical version, in our Qrispy implementation of this quantum +algorithm, a backtracking problem is specified by a maximum recursion depth +and two functions, each returning a :ref:`QuantumBool` respectively: + +**accept**: Is the function that returns True, if called on a node, satisfying the +specifications. + +**reject**: Is the function that returns True, if called on a node, representing a +branch that should no longer be considered. + +Also required is a :ref:`QuantumVariable` that specifies the branches +that can be taken by the algorithm at each node. + +**Node encoding** + +An important aspect of this algorithm is the node encoding. In Montanaro's +paper, a central quantity is the distance from the root $\ell(x)$. We realized that this +doesn't generalize well to the specification of subtrees, which is why +we encode the height of a node. In a tree with maximum depth $n$, for example, a leaf has height 0 and the root has height $n$. + +This quantity is encoded as a one-hot integer QuantumVariable, which can be +found under the attribute ``h``. + +To fully identify a node, we also need to specify the path to take starting +at the root. This path is encoded in a :ref:`QuantumArray`, which can be found +under the attribute ``branch_qa``. To fit into the setting of height encoding, +this array contains the reversed path. + +We summarize the encoding by giving an example: + +In a binary tree with depth 5, the node that has the path from the root [1,1] +is encoded by + +.. math:: + + \begin{align} + \ket{\text{branch_qa}} &= \ket{0}\ket{0}\ket{0}\ket{1}\ket{1}\\ + \ket{\text{h}} &= \ket{3} = \ket{00010}\\ + \ket{x} &= \ket{\text{branch_qa}}\ket{\text{h}} + \end{align} + + +**Details on the predicate functions** + +The predicate functions ``accept`` and ``reject`` must meet certain conditions +for the algorithm to function properly: + +* Both functions have to return a :ref:`QuantumBool`. +* Both functions must not change the state of the tree. +* Both functions must delete/uncompute all temporarily created QuantumVariables. +* ``accept`` and ``reject`` must never return ``True`` on the same node. + +More details for the Qrisp interface to quantum backtracking (including visualisation features) can be found :ref:`here `. + +Quantum backtracking for solving a Sudoku puzzle +------------------------------------------------ + +Now that we understood each separate element of the problem, we can start putting them together. Since most of the quantum backtracking logic is already settled with the Qrisp interface we are just left to implement the ``accept`` and ``reject`` functions. +The first step here is to set-up a Sudoku board. To keep the algorithm still treatable with simulators, we will restrict ourselves to 4x4 Sudokus, however the traditional 9x9 is equally possible. + +:: + + import numpy as np + + sudoku_board = np.array([[ 0, -1, 2, 3], + [ 2, 3, 0, -1], + [ -1, 0, 3, 2], + [ 3, -1, 1, 0]]) + + num_empty_fields = np.count_nonzero(sudoku_board == -1) + +This array represents a Sudoku board with 3 empty fields, that are to be filled. Assuming, that we already have the ``accept`` and ``reject`` functions that we will construct below, we encode this Sudoku puzzle: + +:: + + from qrisp import * + from qrisp.quantum_backtracking import QuantumBacktrackingTree as QBT + + tree = QBT(max_depth = num_empty_fields+1, + branch_qv = QuantumFloat(2), + accept = accept, + reject = reject) + + +Here, the statement ``branch_qv = QuantumFloat(2)`` indicates, that each assignment of the backtracking problem is a 2-qubit integer. These assignments are saved in a :ref: `QuantumArray` of size ``max_depth``. We have to add one additional entry because of reasons that will soon become clear. + +The accept function +^^^^^^^^^^^^^^^^^^^ + +This function is rather simple: A Sudoku board is solved correctly if all entries are filled with numbers that do not contradict the rules of Sudoku. In backtracking language this means, that a node is accepted if it has height $0$ and none of its ancestor nodes were rejected. Thus, the implementation of this function is rather simple: + +:: + + @auto_uncompute + def accept(tree): + return tree.h == 0 + +However, there is a caveat for practical reasons: While Montanaro suggests that the algorithm should never explore rejected nodes, in our implementation rejected nodes are explored but have no children. As described above, we need to pick the depth to be $n = k + 1$ where $k$ is the number of empty fields in the Sudoku board. Otherwise, i.e., if $n = k$, the sibling nodes of the solution might be rejected. Because of this fact, the algorithm will still explore them and evaluate ``accept`` to ``True`` (because they have height 0), leading to the ambiguous situation that a node returns ``True`` for both ``reject`` and ``accept``. + +The reject function +^^^^^^^^^^^^^^^^^^^ + +The ``reject`` function is more complicated because this function needs to consider the Sudoku board and check whether all the assignments are in compliance with the rules of Sudoku. Another layer of complexity is introduced by the fact that the ``reject`` function should only consider entries that have already been assigned. To keep our presentation comprehensive, we will first implement a function, which checks a fully assigned Sudoku board and then modify this function such that it can also ignore non-assigned values. + +Mapping to a graph-coloring problem +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To check the compliance of a fully assigned Sudoku board (encoded in ``branch_qa``), the first step is to transform it into a graph-coloring problem. This implies that we represent each entry of the Sudoku board (given or assigned) as a node of an undirected graph $G$. The rules of Sudoku (columns, rows, and squares containing only distinct entries) are then included by adding an edge to $G$ for each comparison that needs to be performed to assert distinctness of the elements. + + +:: + + import networkx as nx + + def sudoku_to_graph(sudoku_board): + """ + Convert a 4x4 Sudoku problem into a graph coloring problem using networkx. + + Parameters: + - sudoku_board: 4x4 numpy array with numbers 0 to 3 for set fields and -1 for empty fields. + + Returns: + - G: networkx graph representing the Sudoku problem. + - empty_nodes: list of nodes corresponding to the empty fields. + """ + + # Create an empty graph + G = nx.Graph() + empty_nodes = [] + # Add nodes and edges + for i in range(4): + for j in range(4): + if sudoku_board[i, j] == -1: + + # Add node for each empty cell + node = (i, j) + empty_nodes.append(node) + G.add_node(node) + + # Connect to nodes in the same row + for k in range(4): + if k != j: + + # This distincts, wether it is a quantum-quantum or a + # classical quantum comparison. + # Multiple classical-quantum comparisons can be executed + # in a single QuantumDictionary call + if sudoku_board[i,k] == -1: + G.add_edge(node, (i, k), edge_type = "qq") + else: + G.add_edge(node, (i, k), edge_type = "cq") + + # Connect to nodes in the same column + for k in range(4): + if k != i: + if sudoku_board[k,j] == -1: + G.add_edge(node, (k, j), edge_type = "qq") + else: + G.add_edge(node, (k, j), edge_type = "cq") + + # Connect to nodes in the same 2x2 subgrid + subgrid_start_row = (i // 2) * 2 + subgrid_start_col = (j // 2) * 2 + for k in range(subgrid_start_row, subgrid_start_row + 2): + for l in range(subgrid_start_col, subgrid_start_col + 2): + if (k, l) != node: + if sudoku_board[k,l] == -1: + G.add_edge(node, (k, l), edge_type = "qq") + else: + G.add_edge(node, (k, l), edge_type = "cq") + return G, empty_nodes + + +For obvious reasons, we add an edge only if at least one of the participating nodes represents an assigned field. Furthermore, we distinguish between quantum-quantum edges, i.e., a comparison between two empty fields, and classical-quantum edges. This is because for any given node the latter type can be batched together into a single :ref:`QuantumDictionary` call. To capture this fact, we write a helper function, which extracts the comparisons in the following form: + +* quantum-quantum comparisons in the form ``list[(int, int)]`` where the integers indicate the position of the corresponding empty field +* classical-quantum comparisons in the form ``dict({int : list[int]})``. Here the keys of the dictionary indicate the position of the corresponding empty field and the values are the list of numbers to compare to. + +:: + + def extract_comparisons(sudoku_board): + """ + Takes a Sudoku board in the form of a numpy array + where the empty fields are indicated by the value -1. + + Returns two lists: + 1. The quantum-quantum comparisons in the form of a list[(int, int)] + 2. The batched classical-quantum comparisons in the form dict({int : list[int]}) + """ + + num_empty_fields = np.count_nonzero(sudoku_board == -1) + + # Generate the comparison graph + graph, empty_nodes = sudoku_to_graph(sudoku_board) + + # Generate the list of required comparisons + + # This dictionary contains the classical-quantum comparisons for each + # quantum entry + cq_checks = {q_assignment_index : [] for q_assignment_index in range(num_empty_fields)} + + # This dictionary contains the quantum-quantum comparisons as tuples + qq_checks = [] + + # Each edge of the graph corresponds to a comparison. + # We therefore iterate over the edges distinguish between the classical-quantum + # and quantum-quantum comparisons + + for edge in graph.edges(): + edge_type = graph.get_edge_data(*edge)["edge_type"] + + # Append the quantum-quantum comparison to the corresponding list + if edge_type == "qq": + assigment_index_0 = empty_nodes.index(edge[0]) + assigment_index_1 = empty_nodes.index(edge[1]) + + qq_checks.append((assigment_index_0, assigment_index_1)) + + # Append the classical quantum comparison to the corresponding dictionary + elif edge_type == "cq": + + if sudoku_board[edge[1]] == -1: + q_assignment_index = empty_nodes.index(edge[1]) + cq_checks[q_assignment_index].append(sudoku_board[edge[0]]) + else: + q_assignment_index = empty_nodes.index(edge[0]) + cq_checks[q_assignment_index].append(sudoku_board[edge[1]]) + + return qq_checks, cq_checks + +Evaluating the comparisons +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The next step is to evaluate the comparisons to check for element distinctness. This means that we iterate over the edges of the graph and compute a :ref:`QuantumBool` for each edge indicating distinctness of the two connected nodes. +For this, we distinguish between the quantum-quantum and the classical-quantum comparison cases. For the first case, we simply call the ``==`` operator on the two participating quantum variables to compute the comparison :ref:`QuantumBool`. + +:: + + def eval_qq_checks( qq_checks, + q_assigments): + """ + Batched cq_checks is a list of the form + + [(int, int)] + + Where each tuple entry corresponds the index + of the quantum value that should be compared. + q_assigments is a QuantumArray of QuantumFloats, + containing the assignments of the Sudoku field. + """ + # Create result list + res_qbls = [] + + # Iterate over all comparison tuples + # to evaluate the comparisons. + for ind_0, ind_1 in qq_checks: + # Evaluate the comparison + eq_qbl = (q_assigments[ind_0] == + q_assigments[ind_1]) + res_qbls.append(eq_qbl) + + # Return results + return res_qbls + +We test the functionality: + +:: + + q_assigments = QuantumArray(qtype = QuantumFloat(2), shape = (3,)) + + q_assigments[:] = [3,2,3] + + comparison_bools = eval_qq_checks([(0,1), (0,2), (1,2)], q_assigments) + + for qbl in comparison_bools: + print(qbl) + + # Yields + #{False: 1.0} + #{True: 1.0} + #{False: 1.0} + + +As mentioned earlier, classical-quantum comparisons can be batched together to be evaluated in a single function call. This is performed using the :ref:`QuantumDictionary` class. For this, we create a function that receives a :ref:`QuantumVariable` and a list of classical values and returns a :ref:`QuantumBool` indicating, whether the quantum value is contained in the classical list: + +:: + + def cq_eq_check(q_value, cl_values): + """ + Receives a QuantumVariable and a list of classical + values and returns a QuantumBool, indicating whether + the value of the QuantumVariable is contained in the + list of classical values + """ + + if len(cl_values) == 0: + # If there are no values to compare with, we + # return False + return QuantumBool() + + # Create dictionary + qd = QuantumDictionary(return_type = QuantumBool()) + + # Determine the values that q_value can assume + value_range = [q_value.decoder(i) for i in range(2**q_value.size)] + + # Fill dictionary with entries + for value in value_range: + if value in cl_values: + qd[value] = True + else: + qd[value] = False + + # Evaluate dictionary with quantum value + return qd[q_value] + +We test the functionality: + +:: + + q_value = QuantumFloat(2) + q_value[:] = {0 : 1/2**0.5, 1 : 1/2**0.5} + cl_values = [1,2,3] + + res_qbl = cq_eq_check(q_value, cl_values) + + print(res_qbl.qs.statevector()) + # sqrt(2)*(|0>*|False> + |1>*|True>)/2 + + +The next step is to write a function, which performs multiple of these checks and returns a list of :ref:`QuantumBool` similar to the quantum-quantum case. + +:: + + def eval_cq_checks( batched_cq_checks, + q_assigments): + """ + Batched cq_checks is a dictionary of the form + + {int : list[int]} + + Where each key/value pair corresponds to + one batched quantum-classical comparison. + The keys represent the the quantum values + as indices of q_assigments and the values + are the list of classical values that + the quantum value should be compared with. + q_assigments and height are the quantum values + that specify the state of the tree. + """ + # Create result list + res_qbls = [] + + # Iterate over all key/value pairs to evaluate + # the comparisons. + for key, value in batched_cq_checks.items(): + # Evaluate the comparison + eq_qbl = cq_eq_check(q_assigments[key], + value) + res_qbls.append(eq_qbl) + + # Return results + return res_qbls + +We test the functionality: + +:: + + q_assigments = QuantumArray(qtype = QuantumFloat(2), shape = (3,)) + q_assigments[:] = np.arange(3) + + res_qbls = eval_cq_checks({0: [1,2,3], 1 : [1,2,3], 2 : [1,2,3]}, q_assigments) + + for qbl in res_qbls: + print(qbl) + # Yields + # {False: 1.0} + # {True: 1.0} + # {True: 1.0} + + +We can now write the function that checks the Sudoku board. + +:: + + def check_sudoku_assignments(sudoku_board, q_assigments): + """ + Takes a Sudoku board in the form of a numpy array + where the empty fields are indicated by the value -1. + + Furthermore, q_assigments is a QuantumArray of type + type QuantumFloat, describing the assignments. + + The function returns a QuantumBool, indicating whether + the assigments are a valid Sudoku solution. + """ + + num_empty_fields = np.count_nonzero(sudoku_board == -1) + + if num_empty_fields != len(q_assigments): + raise Exception("Number of empty field and length of assigment array disagree.") + + # Generate the comparisons + qq_checks, cq_checks = extract_comparisons(sudoku_board) + + # Evaluate the comparisons + comparison_qbls = [] + + # quantum-quantum + comparison_qbls += eval_qq_checks(qq_checks, q_assigments) + + # classical-quantum + comparison_qbls += eval_cq_checks(cq_checks, q_assigments) + + # Allocate result + sudoku_valid = QuantumBool() + + # Compute the result + mcx(comparison_qbls, sudoku_valid, ctrl_state = 0, method = "balauca") + + return sudoku_valid + + +We test the functionality: + +:: + + q_assignments = QuantumArray(qtype = QuantumFloat(2), shape = (4,)) + q_assignments[:] = [1,1,1,2] + + sudoku_check = check_sudoku_assignments(sudoku_board, q_assignments) + print(sudoku_check) + # Yields {True: 1.0} + + # Another check + + q_assignments = QuantumArray(qtype = QuantumFloat(2), shape = (4,)) + q_assignments[:] = [1,2,1,0] + + sudoku_check = check_sudoku_assignments(sudoku_board, q_assignments) + print(sudoku_check) + # Yields {False: 1.0} + + +So far so good! This could have already been used in a Grover based implementation, but as discussed before, we want to utilize the **structure** of the problem! + +Adaptation for Quantum Backtracking +----------------------------------- + +As this is a backtracking implementation, our Sudoku compliance check also has to understand that the results of certain comparisons should be ignored, since the corresponding fields are not assigned yet. For example, consider a Sudoku field with 4 empty fields, where only one field has been assigned so far. In our implementation of the algorithm, the empty fields are encoded as zeros in ``branch_qa`` and we only know that they are not assigned yet by considering the height :ref:`QuantumVariable`. The implementation of the Sudoku-check algorithm given above would therefore return "not valid" for almost every single node, because it assumes that the 3 remaining empty fields carry the value 0 even though in reality they have not been assigned yet. Because of that we need to also take the value of the height variable ``h`` into consideration, describing the height of the node in the :ref:`QuantumBacktrackingTree`. + +Fortunately, the one-hot encoding of this variable makes this rather easy: The value that has been assigned most recently is indicated by the corresponding qubit in ``h`` being in the $\ket{1}$ state. For example, in a tree of maximum depth 5, if the ``branch_qa`` entry with height 3 has been assigned recently, ``h`` will be in the state $000100$. The next assignment would then be height 2, i.e. $001000$. +For a quantum-classical comparison with the ``branch_qa`` entry $i$, we can therefore simply call the comparison evaluation controlled on the $i$-th qubit in ``h``. This implies that this comparison can only result in ``True``, and as a result cause the ``reject`` value to be ``True`` if $i$ was assigned most recently. + +We reformulate the classical comparison function: + +:: + + def eval_cq_checks( batched_cq_checks, + q_assigments, + h): + """ + Batched cq_checks is a dictionary of the form + + {int : list[int]} + + Each key/value pair corresponds to + one batched quantum-classical comparison. + The keys represent the the quantum values + as indices of q_assigments and the values + are the list of classical values that + the quantum value should be compared with. + q_assigments and height are the quantum values + that specify the state of the tree. + """ + # Create result list + res_qbls = [] + + # Iterate over all key/value pairs to evaluate + # the comparisons. + for key, value in batched_cq_checks.items(): + # Enter the control environment + with control(h[key]): + # Evaluate the comparison + eq_qbl = cq_eq_check(q_assigments[key], + value) + res_qbls.append(eq_qbl) + + # Return results + return res_qbls + +The code example above demonstrates a function that takes a dictionary representing the batched quantum-classical equality checks, the ``QuantumArray branch_qa``, and the :ref:`QuantumVariable` ``h`` as input. It returns a list of of :ref:`QuantumBool` that represent the result of the comparisons. Note the line ``with control(h[key]):`` which enters a :ref:`ControlEnvironment`. This means that every quantum instruction that happens in the indented area is controlled on the qubit ``h[key]``. As described above, this feature ensures that the comparison of values that are not assigned yet cannot contribute to the result of the ``reject`` function. + +We adopt a similar approach for the quantum-quantum comparison. For a comparison between the $i$-th and $j$-th position, we control the comparison on the $k$-th qubit of the ``h`` variable where $k = \text{min}(i,j)$. This way only comparisons are executed on recently assigned variables, preventing rejections for cases involving variables that are either not assigned at all or not recently assigned. For more details, consult the corresponding section of the paper. + + +:: + + def eval_qq_checks( qq_checks, + q_assigments, + h): + """ + Batched cq_checks is a list of the form + + [(int, int)] + + Where each tuple entry corresponds the index + of the quantum value that should be compared. + branch_qa and height are the quantum values + that specify the tree state. + """ + # Create result list + res_qbls = [] + + # Iterate over all comparison tuples + # to evaluate the comparisons. + for ind_0, ind_1 in qq_checks: + # Enter the control environment + with control(h[min(ind_0, ind_1)]): + # Evaluate the comparison + eq_qbl = (q_assigments[ind_0] == + q_assigments[ind_1]) + res_qbls.append(eq_qbl) + + # Return results + return res_qbls + +Similarly to the previous case, we can now create the Sudoku checking function but this time ignoring all the non-assigned values. + +:: + + def check_singular_sudoku_assignment(sudoku_board, q_assigments, h): + """ + Takes the following arguments: + + 1. sudoku_board is Sudoku board in the form of a numpy array + where the empty fields are indicated by the value -1. + + 2. q_assigments is a QuantumArray of type + type QuantumFloat, describing the assignments. + + 3. h is a one-hot encoded QuantumVariable representing, which + assignment should be checked for validity + + The function returns a QuantumBool, indicating whether + the assigment indicated by h respects the rules of Sudoku. + """ + + num_empty_fields = np.count_nonzero(sudoku_board == -1) + + if num_empty_fields != len(q_assigments): + raise Exception("Number of empty field and length of assigment array disagree.") + + # Generate the comparisons + qq_checks, cq_checks = extract_comparisons(sudoku_board) + + # Evaluate the comparisons + comparison_qbls = [] + + # quantum-quantum + comparison_qbls += eval_qq_checks(qq_checks, q_assigments, h) + + # classical-quantum + comparison_qbls += eval_cq_checks(cq_checks, q_assigments, h) + + # Allocate result + sudoku_valid = QuantumBool() + + # Compute the result + mcx(comparison_qbls, sudoku_valid, ctrl_state = 0, method = "balauca") + + return sudoku_valid + +We can now test it: + +:: + + q_assigments = QuantumArray(qtype = QuantumFloat(2), shape = (4,)) + q_assigments[:] = [0,0,1,2] + + from qrisp.quantum_backtracking import OHQInt + + h = OHQInt(4) + h[:] = 2 + + test_qbl = check_singular_sudoku_assignment(sudoku_board, q_assigments, h) + + print(test_qbl) + # Yields {True: 1.0} + +Even though the first two entries are 0 and they are in the same quadrant, their comparisons is not evaluated so our function still returns ``True`` because the assignment corresponding to height 2 passes all the checks. We can repeat the experiment with an invalid assignment at height 2. + +:: + + q_assigments = QuantumArray(qtype = QuantumFloat(2), shape = (4,)) + q_assigments[:] = [0,0,2,2] + + from qrisp.quantum_backtracking import OHQInt + + h = OHQInt(4) + h[:] = 2 + + test_qbl = check_singular_sudoku_assignment(sudoku_board, q_assigments, h) + + print(test_qbl) + # Yields {False: 1.0} + +We can therefore now finally formulate our reject function: + +:: + + @auto_uncompute + def reject(tree): + + # Cut off the assignment with height 0 + # since it is not relevant for the sudoku + # checker + q_assigments = tree.branch_qa[1:] + + # Modify the height to reflect the cut off + modified_height = tree.h[1:] + + assignment_valid = check_singular_sudoku_assignment(sudoku_board, + q_assigments, + modified_height) + return assignment_valid.flip() + + +Finding a solution +^^^^^^^^^^^^^^^^^^ + +Finally, with the accept and reject funtions, we can encode our Sudoku puzzle as a backtracking tree and **detect** the existence of a solution. +For this, the tree is initialized in the state $\ket{r}$ (indicating the root) and quantum phase estimation (QPE) for the quantum walk operator with the specified ``precision`` is applied. +The algorithm returns "Solution exists" if the 0 component of the quantum phase estimation result +has a higher probability then 3/8 = 0.375. If the probability is less than 0.25, the algorithm returns "No solution exists". Otherwise, the precision of the phase estimation has to be increased. To make the result still simulable on a laptop, we will decrease the amount of empty fields to 3. If you want to try higher more empty fields, we recommend using the IBM cloud MPS simulator. Find out how to deploy it in Qrisp :ref:`Qrisp101`. + +:: + + # Decrease the empty field count + sudoku_board = np.array([[ 0, -1, 2, 3], + [ 2, 3, 0, -1], + [ 1, 0, 3, 2], + [ 3, -1, 1, 0]]) + + num_empty_fields = np.count_nonzero(sudoku_board == -1) + + + from qrisp import * + from qrisp.quantum_backtracking import QuantumBacktrackingTree as QBT + + tree = QBT(max_depth = num_empty_fields+1, + branch_qv = QuantumFloat(2), + accept = accept, + reject = reject, + subspace_optimization = True) + + # Initialize root + tree.init_node([]) + + #Perform QPE + qpe_res = tree.estimate_phase(precision = 3) + + # Retrieve measurements + mes_res = qpe_res.get_measurement() + + + if mes_res[0]>0.375: + print("Solution exists") + elif mes_res[0]<0.25: + print("No solution exists") + else: + print("Insufficent precision") + + +To **find** a solution, we employ the ``find_solution`` method. This method starts by applying the ``estimate_phase`` function to the entrire tree (initialized in the state $\ket{r}$) and, based on the (multi-) measurement results, recursively applies the ``estimate_phase`` function to subtrees in order to find a solution. +Note that, in order to achieve a speed-up in practical scenarios, it is necessary to specify the ``precision`` and the number of measurements (by default 10000) for the ``estimate_phase`` method accordingly. + +:: + + from qrisp import * + from qrisp.quantum_backtracking import QuantumBacktrackingTree as QBT + + tree = QBT(max_depth = num_empty_fields+1, + branch_qv = QuantumFloat(2), + accept = accept, + reject = reject, + subspace_optimization = True) + + sol = tree.find_solution(precision = 3) + print(sol[::-1][1:]) + # Yields [1, 1, 2] + +With this, we can find the solution for Sudoku problems with up to 3 empty fields with the statevector simulator on our local computer. For instances with more empty fields, we can still find the solution with a matrix product state simulator that can be employd with the ``measurement_kwargs`` keyword. + +Well done on completing our quantum Sudoku-solving tutorial! You're now part of an exclusive club, as this is the only guide of its kind available online. Pretty cool, huh? Remember, what makes quantum computing so exciting is how it taps into the unique structure of problems (like how we utilized the problem structure above). By understanding this, you're diving headfirst into a world where quantum algorithms could outshine their classical counterparts. \ No newline at end of file diff --git a/documentation/source/general/tutorial/index.rst b/documentation/source/general/tutorial/index.rst index 27f82567..55595d40 100644 --- a/documentation/source/general/tutorial/index.rst +++ b/documentation/source/general/tutorial/index.rst @@ -30,6 +30,7 @@ By the end of this tutorial, you'll have a solid foundation of our high-level fr tutorial Quantum Alternating Operator Ansatz/index TSP + Sudoku Shor FT_compilation diff --git a/documentation/source/general/tutorial/sudoku.png b/documentation/source/general/tutorial/sudoku.png new file mode 100644 index 00000000..e0a84aa9 Binary files /dev/null and b/documentation/source/general/tutorial/sudoku.png differ diff --git a/requirements.txt b/requirements.txt index 96bf5c4c..1d6ba75e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ connexion>=2.12.0 -qiskit>=0.34.2 +qiskit<=0.44 thrift>=0.15.0 matplotlib>=3.5.1 waitress>=2.1.1 diff --git a/setup.cfg b/setup.cfg index 0b881ea7..425cdd95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = qrisp -version = 0.4.3 +version = 0.4.4 author = Raphael Seidel author_email = raphael.seidel@fokus.fraunhofer.de description = A high-level quantum programming language diff --git a/src/qrisp/arithmetic/comparisons.py b/src/qrisp/arithmetic/comparisons.py index 49dfd595..621678d3 100644 --- a/src/qrisp/arithmetic/comparisons.py +++ b/src/qrisp/arithmetic/comparisons.py @@ -18,7 +18,7 @@ from qrisp.misc.utility import lifted -from qrisp.environments import adaptive_condition +from qrisp.environments import adaptive_condition, conjugate def less_than_gate(a, b): @@ -128,7 +128,7 @@ def less_than(a, b): if isinstance(a, QuantumFloat) and isinstance(b, QuantumFloat): lt_gate = less_than_gate(a, b) - lt_qbl = QuantumBool() + lt_qbl = QuantumBool(qs = a.qs, name = "lt_qbl*") anc_amount = lt_gate.num_qubits - a.size - b.size - 1 @@ -163,7 +163,7 @@ def less_than(a, b): lt_gate = less_than_gate(a, b) - lt_qbl = QuantumBool() + lt_qbl = QuantumBool(qs = a.qs, name = "lt_qbl*") anc_amount = lt_gate.num_qubits - a.size - 1 @@ -205,7 +205,7 @@ def less_than(a, b): def equal(qf_0, qf_1): from qrisp import QuantumBool, QuantumFloat, cx, mcx - eq_qbl = QuantumBool() + eq_qbl = QuantumBool(qs = qf_0.qs, name = "eq_qbl*") if isinstance(qf_1, QuantumFloat): if qf_1.signed and not qf_0.signed: @@ -226,17 +226,17 @@ def equal(qf_0, qf_1): significance_dict[qf_0.exponent + i] = qf_0[i] mcx_qubits.append(qf_0[i]) + def conjugator(qf_1, significance_dict): + for i in range(qf_1.msize): + if i + qf_1.exponent in significance_dict: + cx(qf_1[i], significance_dict[i + qf_1.exponent]) + for i in range(qf_1.msize): - if i + qf_1.exponent in significance_dict: - cx(qf_1[i], significance_dict[i + qf_1.exponent]) - else: + if i + qf_1.exponent not in significance_dict: mcx_qubits.append(qf_1[i]) - mcx(mcx_qubits, eq_qbl, ctrl_state=0) - - for i in range(qf_1.msize): - if i + qf_1.exponent in significance_dict: - cx(qf_1[i], significance_dict[i + qf_1.exponent]) + with conjugate(conjugator)(qf_1, significance_dict): + mcx(mcx_qubits, eq_qbl, ctrl_state=0) if qf_1.signed and qf_0.signed: cx(qf_1.sign(), qf_0.sign()) diff --git a/src/qrisp/core/library.py b/src/qrisp/core/library.py index 17623c37..77bd9e5b 100644 --- a/src/qrisp/core/library.py +++ b/src/qrisp/core/library.py @@ -464,12 +464,28 @@ def benchmark_mcx(n, methods): """ - from qrisp.circuit.quantum_circuit import convert_to_qb_list from qrisp.misc import bin_rep from qrisp.mcx_algs import GidneyLogicalAND, amy_toffoli, jones_toffoli + from qrisp.core import QuantumVariable + from qrisp.qtypes import QuantumBool - qubits_0 = convert_to_qb_list(controls) - qubits_1 = convert_to_qb_list(target) + new_controls = [] + + for qbl in controls: + if isinstance(qbl, QuantumBool): + new_controls.append(qbl[0]) + else: + new_controls.append(qbl) + + if isinstance(target, (list, QuantumVariable)): + + if len(target) > 1: + raise Exception("Target of mcx contained more than one qubit") + target = target[0] + + + qubits_0 = new_controls + qubits_1 = [target] n = len(qubits_0) diff --git a/src/qrisp/default_backend.py b/src/qrisp/default_backend.py index 2dfe545a..66b815f3 100644 --- a/src/qrisp/default_backend.py +++ b/src/qrisp/default_backend.py @@ -38,4 +38,5 @@ def run(self, qc, shots, token = ""): def_backend = DefaultBackend() -# def_backend = BackendClient() \ No newline at end of file +# def_backend = BackendClient() +# raise \ No newline at end of file diff --git a/src/qrisp/environments/conjugation_environment.py b/src/qrisp/environments/conjugation_environment.py index 7d57d985..22f125da 100644 --- a/src/qrisp/environments/conjugation_environment.py +++ b/src/qrisp/environments/conjugation_environment.py @@ -181,7 +181,7 @@ def __exit__(self, exception_type, exception_value, traceback): @custom_control - def perform_conjugation(self, ctrl = None): + def perform_conjugation(self, ctrl = None, ctrl_method = None): temp = list(self.env_qs.data) self.env_qs.data = [] @@ -192,7 +192,7 @@ def perform_conjugation(self, ctrl = None): # self.env_qs.data.extend(self.conjugation_circ.data) if ctrl is not None: - with control(ctrl): + with control(ctrl, ctrl_method = ctrl_method): self.env_qs.data.extend(temp) else: diff --git a/src/qrisp/environments/custom_control_environment.py b/src/qrisp/environments/custom_control_environment.py index 914ce207..6c075e75 100644 --- a/src/qrisp/environments/custom_control_environment.py +++ b/src/qrisp/environments/custom_control_environment.py @@ -16,6 +16,8 @@ ********************************************************************************/ """ +import inspect + from qrisp.environments.quantum_environments import QuantumEnvironment from qrisp.environments.gate_wrap_environment import GateWrapEnvironment from qrisp.circuit import Operation, QuantumCircuit, Instruction @@ -162,12 +164,19 @@ def adaptive_control_function(*args, **kwargs): # If no control qubit was found, simply execute the function if control_qb is None: return func(*args, **kwargs) + + # Check whether the function supports the ctrl_method kwarg and adjust + # the kwargs accordingly + if "ctrl_method" in list(inspect.getargspec(func))[0] and isinstance(env, ControlEnvironment): + kwargs.update({"ctrl_method" : env.ctrl_method}) + # In the case that a qubit was found, we use the CustomControlEnvironent (definded below) # This environments gatewraps the function and compiles it to a specific Operation subtype # called CustomControlledOperation. # The Condition/Control Environment compiler recognizes this Operation type # and processes it accordingly + with CustomControlEnvironment(control_qb, func.__name__): if "ctrl" in kwargs: kwargs["ctrl"] = control_qb diff --git a/src/qrisp/interface/backends.py b/src/qrisp/interface/backends.py index 3caa7ab3..45b0e008 100644 --- a/src/qrisp/interface/backends.py +++ b/src/qrisp/interface/backends.py @@ -154,8 +154,6 @@ class VirtualQiskitBackend(VirtualBackend): """ - from qiskit import Aer - def __init__(self, backend=None, port=8079): if backend is None: from qiskit import Aer diff --git a/src/qrisp/qaoa/problems/MaxIndep_qiroBM.py b/src/qrisp/qaoa/problems/MaxIndep_qiroBM.py new file mode 100644 index 00000000..fca4a852 --- /dev/null +++ b/src/qrisp/qaoa/problems/MaxIndep_qiroBM.py @@ -0,0 +1,67 @@ +import qrisp + +from qrisp.qiroStuff.qiroProblem import QIROProblem + +from qrisp.qiroStuff.qiroMaxIndepSetInfrastr import * + +from qrisp.qaoa.qaoa_problem import QAOAProblem +from qrisp.qaoa.problems.create_rdm_graph import create_rdm_graph +from qrisp.qaoa.problems.maxIndepSetInfrastr import maxIndepSetCostOp, maxIndepSetclCostfct +from qrisp.qaoa.mixers import RX_mixer +from qrisp import QuantumVariable +from qrisp.qaoa.qaoa_benchmark_data import approximation_ratio +import matplotlib.pyplot as plt +import numpy as np +import networkx as nx +import itertools + +# qiskit mps simulator + +optimal_sols = "10000000000000000000" +num_nodes = 20 + +recursive_array = [] +normal_array = [] + + +for index in range(30): + giraf = create_rdm_graph(num_nodes,0.3, seed = index*3) + qarg = QuantumVariable(giraf.number_of_nodes()) + + maxindep_instance = QAOAProblem(maxIndepSetCostOp(giraf), RX_mixer, maxIndepSetclCostfct(giraf)) + the_recursive = QIROProblem(giraf, maxindep_instance, + replacement_routine=create_maxIndep_replacement_routine, + qiro_cost_operator= create_maxIndep_cost_operator_reduced, + qiro_mixer= create_maxIndep_mixer_reduced, + qiro_init_function= init_function_reduced + ) + + res, solutions, exclusions, corr_vals = the_recursive.run_new_idea(qarg=qarg, depth = 3, n_recursions = 1) + testCostFun = maxIndepSetclCostfct(giraf) + + approx_rat = approximation_ratio(res, optimal_sols,testCostFun) + print(approx_rat) + recursive_array.append(approx_rat) + qarg.delete() + + +for index2 in range(30): + + giraf = create_rdm_graph(num_nodes,0.3, seed = index2*3) + + qarg = QuantumVariable(giraf.number_of_nodes()) + + maxindep_instance = QAOAProblem(maxIndepSetCostOp(giraf), RX_mixer, maxIndepSetclCostfct(giraf)) + res = maxindep_instance.run(qarg=qarg, depth = 5) + testCostFun = maxIndepSetclCostfct(giraf) + + approx_rat = approximation_ratio(res, optimal_sols,testCostFun) + print(approx_rat) + normal_array.append(approx_rat) + qarg.delete() + +print("recursive") +print(sum(recursive_array)/len(recursive_array)) + +print("normal") +print(sum(normal_array)/len(normal_array)) \ No newline at end of file diff --git a/src/qrisp/qaoa/problems/QUBO.py b/src/qrisp/qaoa/problems/QUBO.py index 5a1d5651..27ac1584 100644 --- a/src/qrisp/qaoa/problems/QUBO.py +++ b/src/qrisp/qaoa/problems/QUBO.py @@ -77,15 +77,7 @@ def create_QUBO_cost_operator(Q): cost_operator function. """ - #def QUBO_cost_operator(qv, gamma): - - # for i in range(len(Q)): - # rz(-0.5*2*gamma*(0.5*Q[i][i]+0.5*sum(Q[i])), qv[i]) - # for j in range(i+1, len(qv)): - # if Q[i][j] !=0: - # rzz(0.25*2*gamma*Q[i][j], qv[i], qv[j]) - #return QUBO_cost_operator - #new try + def QUBO_cost_operator(qv, gamma): gphase(-gamma/4*(np.sum(Q)+np.trace(Q)),qv[0]) @@ -119,7 +111,7 @@ def QUBO_problem(Q): return QAOAProblem(create_QUBO_cost_operator(Q), RX_mixer, create_QUBO_cl_cost_function(Q)) -def solve_QUBO(Q, depth, backend = None, n_solutions = 1, print_res = True): +def solve_QUBO(Q, depth, max_iter = 50, backend = None, n_solutions = 1, print_res = False): """ Solves a Quadratic Unconstrained Binary Optimization (QUBO) problem using the Quantum Approximate Optimization Algorithm (QAOA). The function imports the default backend from the 'qrisp.default_backend' module. @@ -134,6 +126,8 @@ def solve_QUBO(Q, depth, backend = None, n_solutions = 1, print_res = True): QUBO matrix to solve. depth : int The depth (amount of layers) of the QAOA circuit. + max_iter : int + The maximal amount of iterations of the COBYLA optimizer in the QAOA algorithm. backend : str The backend to be used for the quantum/annealing simulation. n_solutions : int @@ -141,8 +135,8 @@ def solve_QUBO(Q, depth, backend = None, n_solutions = 1, print_res = True): Returns ------- - None - The function prints the runtime of the QAOA algorithm and the ``n_solutions`` best solutions with their respective costs. + optimal_solution: tuple + The function returns the optimal solution as a tuple where the first element is the cost and the second element is the optimal bitstring. If print_res is set to True, the function prints the runtime of the QAOA algorithm and the ``n_solutions`` best solutions with their respective costs. """ @@ -159,7 +153,7 @@ def solve_QUBO(Q, depth, backend = None, n_solutions = 1, print_res = True): backend = backend # Run QAOA with given quantum arguments, depth, measurement keyword arguments and maximum iterations for optimization - res = QUBO_instance.run(qarg, depth, mes_kwargs={"backend" : backend}, max_iter = 50) # runs the simulation + res = QUBO_instance.run(qarg, depth, mes_kwargs={"backend" : backend}, max_iter = max_iter) # runs the simulation res = dict(list(res.items())[:n_solutions]) # Calculate the cost for each solution @@ -168,7 +162,12 @@ def solve_QUBO(Q, depth, backend = None, n_solutions = 1, print_res = True): # Sort the solutions by their cost in ascending order sorted_costs_and_solutions = sorted(costs_and_solutions, key=itemgetter(0)) + optimal_solution = sorted_costs_and_solutions[0] + if print_res is True: # Get the top solutions and print them for i in range(n_solutions): - print(f"Solution {i+1}: {sorted_costs_and_solutions[i][1]} with cost: {sorted_costs_and_solutions[i][0]}") \ No newline at end of file + print(f"Solution {i+1}: {sorted_costs_and_solutions[i][1]} with cost: {sorted_costs_and_solutions[i][0]}") + + return optimal_solution + diff --git a/src/qrisp/quantum_backtracking/backtracking_tree.py b/src/qrisp/quantum_backtracking/backtracking_tree.py index 24182c67..c4a047bb 100644 --- a/src/qrisp/quantum_backtracking/backtracking_tree.py +++ b/src/qrisp/quantum_backtracking/backtracking_tree.py @@ -703,8 +703,16 @@ def accept(tree): # into a temporary container. This way the branching information is 0. # Check if |x> is root. - is_root = QuantumBool() - cx(self.h[self.max_depth],is_root) + + + # This + if self.max_depth%2 == even: + cx(self.h[self.max_depth],oddity_qbl) + + # Instead of this + + # is_root = QuantumBool() + # cx(self.h[self.max_depth],is_root) temporary_container = self.branch_qa.qtype.duplicate() @@ -730,8 +738,8 @@ def accept(tree): ctrl_state += "0" # Check if |x> is root. Otherwise, if the reject funtions returns "True" on the lift of the root a wrong phase (-1) may be applied to the root. - mcz_list.append(is_root) - ctrl_state += "0" + # mcz_list.append(is_root) + # ctrl_state += "0" # Add extra controls @@ -1735,15 +1743,14 @@ def __init__(self, parent_tree, root_path): "Tried to initialise subtree with root path longer than maximum depth") QuantumBacktrackingTree.__init__(self, - parent_tree.max_depth - \ - len(root_path), + parent_tree.max_depth, parent_tree.branch_qa[0], parent_tree.accept_function, - parent_tree.reject_function + parent_tree.reject_function, + parent_tree.subspace_optimization ) - self.embedding_array = QuantumArray(qtype = self.branch_qa.qtype, shape = len(root_path), qs = self.qs) - self.branch_qa = np.concatenate((self.branch_qa, self.embedding_array)) + self.max_depth = parent_tree.max_depth - len(root_path) self.root_path = root_path self.original_tree = parent_tree @@ -1783,7 +1790,7 @@ def init_phi(self, path): rev_branch_qa[k+i][:] = self.root_path[k] def subtree(self, path): - return self.original_tree.subtree(self.root_path + path) + return self.original_tree.subtree(path) @@ -1814,7 +1821,7 @@ def cl_accept(path): path = [] if cl_accept(path): - return [] + return path elif tree.max_depth == 0: return None @@ -1870,23 +1877,26 @@ def cl_accept(path): for b in new_branches: # Get the path to the new node - new_path=tree.path_decoder(b[1], b[2]) + if isinstance(tree, Subtree): + new_path=tree.original_tree.path_decoder(b[1], b[2]) + else: + new_path=tree.path_decoder(b[1], b[2]) - if tuple(path + new_path) in traversed_nodes or len(new_path) == 0: - continue + # Continue if new_path was already explored + if tuple(new_path) in traversed_nodes or tuple(new_path)==tuple(path): + continue # Generate the subtree subtree=tree.subtree(new_path) # Recursive call - sub_sol=find_solution(subtree, precision, cl_accept, traversed_nodes) + solution=find_solution(subtree, precision, cl_accept, traversed_nodes, measurement_kwargs=measurement_kwargs) - # If a solution has been found, concatenate the pathes - if sub_sol is not None: - solution=path + new_path + sub_sol + # Leave loop if solution was found + if solution is not None: break else: - traversed_nodes.append(tuple(path + new_path)) + traversed_nodes.append(tuple(new_path)) else: raise Exception( diff --git a/src/qrisp/simulator/bi_array_helper.py b/src/qrisp/simulator/bi_array_helper.py index 9050f672..090bdcc1 100644 --- a/src/qrisp/simulator/bi_array_helper.py +++ b/src/qrisp/simulator/bi_array_helper.py @@ -397,7 +397,7 @@ def coo_sparse_matrix_mult_jitted(A_row, A_col, A_data, B_row, B_col, B_data, A_ for i in range(res.shape[0]): for j in range(res.shape[1]): - if np.abs(res[i,j]) > 1E-10: + if np.abs(res[i,j]) > float_tresh: # if res[i,j] != 0: new_row.append(A_row[unique_marker_a[i]]) new_col.append(B_col[unique_marker_b[j]]) diff --git a/src/qrisp/simulator/tensor_factor.py b/src/qrisp/simulator/tensor_factor.py index 0e177ae2..ae196ec2 100644 --- a/src/qrisp/simulator/tensor_factor.py +++ b/src/qrisp/simulator/tensor_factor.py @@ -75,10 +75,7 @@ def apply_matrix(self, matrix, qubits): import numpy as np if matrix.size >= 2**6: - if matrix.dtype == np.complex64: - matrix = matrix * (np.abs(matrix) > 1e-7) - if matrix.dtype == np.complex128: - matrix = matrix * (np.abs(matrix) > 1e-15) + matrix = matrix * (np.abs(matrix) > float_tresh) # Convert matrix to BiArray matrix = DenseBiArray(matrix) diff --git a/tests/test_qiskit_backend_client.py b/tests/test_qiskit_backend_client.py index 15b42a79..481ea786 100644 --- a/tests/test_qiskit_backend_client.py +++ b/tests/test_qiskit_backend_client.py @@ -22,12 +22,12 @@ from qrisp.core import QuantumSession from qrisp.interface.backends import VirtualBackend from qrisp.interface.backends import VirtualQiskitBackend -from qiskit import Aer + def test_qiskit_backend_client(): # TO-DO prevent this test from crashing regardless of functionality - return + from qiskit import Aer # Create QuantumSession qs = QuantumSession() diff --git a/tests/test_unitary_calculation.py b/tests/test_unitary_calculation.py index 6ee8053d..b912bcc3 100644 --- a/tests/test_unitary_calculation.py +++ b/tests/test_unitary_calculation.py @@ -22,11 +22,12 @@ from qrisp.core import QuantumSession from qrisp.arithmetic import QuantumFloat from numpy.linalg import norm -from qiskit import execute, Aer + from qrisp.interface import convert_circuit def test_unitary_calculation(): + from qiskit import execute, Aer n = 2 qs = QuantumSession()