"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""
from itertools import product
import numpy as np
import networkx as nx
from sympy.physics.quantum import Ket, OrthogonalKet
from qrisp import (QuantumFloat, QuantumBool, QuantumArray, mcz, cx, h, ry, swap, QFT,
auto_uncompute, invert, control, IterationEnvironment, bin_rep,
cyclic_shift, multi_measurement, increment, xxyy, p, QuantumVariable, cz,
mcx, z, x, RYGate, HGate, s, t, s_dg, t_dg)
"""
As specified in the paper (https://arxiv.org/abs/1509.02374), the key challenge
in implementing the quantum backtracking algorithm is the realization of the operators
R_A and R_B. These operators consists of the direct sum of diffuser operators D_x,
where x is an arbitrary node of the backtracking graph.
R_A and R_B are defined as the direct sum of these
R_A = [direct sum] D_x [summed over all nodes x with even depth]
R_B = |r><r| [direct sum] D_x [summed over all nodes x with even depth]
Or in words: Each node x together with it's children {y : x->y}
defines a subspace H_x = span(|x>, {|y>, x->y}) on which the operator D_x is acting
in-place.
The definition of the D_x operators consists of multiple conditions:
1. If x is an "accept" node, D_x = 1 (ie. the identity)
2. If x is the root D_x = 1 - 2|psi_r><psi_r|
Where |psi_r> = (1+d_r*n)**-0.5 * (|r> + n**0.5 * [sum] |y>)
Where d_r is the degree of the root, n is the maximum depth of the tree
and the sum iterates over all children of r
3. Otherwise: D_x = 1 - 2 |psi_x><psi_x|
Where |psi_x> = (d_x)**-0.5 (|x> + [sum] |y>)
Where d_x is the degree of x and the sum iterates over all children of x
To implement this operator, we will rewrite it a bit:
D_x = 1 - (1+(-1)**accept(x)) * |psi_r><psi_r|
The next step is to assume an operator U_x, which prepares |psi_x> from x
|psi_x> = U_x |x>
We can then write
D_x = D_x = 1 - (1+(-1)**accept(x)) * |psi_r><psi_r|
= U_x (1 - (1+(-1)**accept(x))*|x><x|) U_x^(-1)
If we pick an encoding, where each node state |x> is a computational basis state,
the center bracket (1 - (1+(-1)**accept(x))*|x><x|) can be realized as a
Z-gate on the bitstring on the
result of accept(x).
Furthermore we need to make sure that only the phase of |x> is flipped. If we simply
apply a Z-gate onto the result of accept, we could also flip the phase of a
child node y with accept(y) = True. We remedy this problem by determining wether
|x> is odd into a qubit called oddity. For the even diffusing function, this qubit
needs to be in the False state, for the odd function this qubit needs to be in the
True state.
This qubit is then used in a CZ gate.
D_x = U_x CZ(accept(x), oddity(x)) U_x^(-1)
The operator U_x is implemented as the function psi_prep below.
The operator D_x is implemented as the method qstep_diffuser of the
QuantumBacktrackingTree class.
The next layer of complexity is the reject function.
If a node is rejected, it has no children, ie. |psi_x> = |x>.
This could be realized by modifiying the operator U_x but as it turns out there is
a more efficient possibility.
For this consider the fact that for a rejected node
D_x = 1 - 2 |psi_x><psi_x| = 1 - 2 |x><x|
The algorithm will however never evaluate this operator on any of the children of x
so restricted on H_x we can assume
D_x = -1
We therfore now need to modify D_x such that D_x = -1 when reject(x) is True.
To achieve this we deploy a circuit that integrates with our previous construction
but instead performs
D_x = U_x (-1) U_x^(-1)
if reject(x) = True.
Our goal is therefore now to find some operator O such that
CZ(accept(x), oddity(x)) O(x) = [-1 if reject(x) is True]
[CZ(accept(x), oddity(x)) if reject(x) is False]
We conclude that O(reject(x)) needs to flip the phases of all the (pseudo) child states.
(because H_x = span(|x>, {|y>, x->y})).
Similar to the above approach we reuse the oddity qubit to make sure we only operate
on the child states.
Unfortunately we can't assume that the reject function also returns True on the children,
therefore we "lift" the state. This means that we evaluate the reject function on the
parent. We denote this with the symbol ^.
We conclude:
O(x) = CZ(reject(x^), not oddity(x))
To understand how the lifting works, please check the implementation of qstep_diffuser.
This brings us to another important point in the implementation of the algorithm:
The encoding of the node states.
In principle, the paper makes no statement how such an encoding could be realized.
The approach we took here is the following:
A node state |x> is specified by:
1. A one-hot encoded integer h (for height), which specifies the distance of
x from the the steepest leaf.
2. A QuantumArray branch_qa, which specifies the path to take to reach x.
A few things have to be said about this encoding:
We choose to encoded the height instead of the distance of the root
(as described in the paper), as this allows straight forward generation of subtrees.
A node which has height 4 in a some tree T, still has height 4 in a subtree of T.
The initial state of branch_qa is |0>|0>|0>...
Because we use the height instead of the path length, the path to the node is
specified by the reveresed array. Ie. the path [1,1,0,1] in a depth 7 tree is
specified by the branch_qa state
|0>|0>|0>|1>|0>|1>|1>
Any state where there is a non-zero state at an index lower than h is considered
"non-algorithmic", ie. the state does not represent a node. An example could be
h = |4>
branch_qa = |0>|0>|1>|0>|0>|0>
"""
[docs]
class QuantumBacktrackingTree:
r"""
This class describes the central data structure to run backtracking algorithms in
a quantum setting. `Backtracking algorithms <https://en.wikipedia.org/wiki/Backtracking>`_
are a very general class of algorithms which cover many problems of combinatorial
optimization such as 3-SAT or TSP.
Backtracking algorithms can be put into a very general form. Given is a maximum
recursion depth, two functions called ``accept``/``reject`` and the set of
possible assignments for an iterable x.
::
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
The power of these algorithms lies in the fact that they can quickly discard
large parts of the potential solution space by using the reject function to
cancel the recursion. Compared to an unstructured search, where only the
accept function is available, this can significantly cut the required resources.
The quantum algorithm for solving these problems has been
`proposed by Ashley Montanaro <https://arxiv.org/abs/1509.02374>`_ and yields
a 1 to 1 correspondence between an arbitrary classical backtracking algorithm
and it's 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
"Node 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, for the Qrisp 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.
Furthermore 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 Montanaros
paper a central quantity is the distance from the root $l(x)$. This however
doesn't generalize well to the specification of subtrees, which is why
we encode the height of a node. For example in a tree with maximum depth $n$
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.
The ``subspace_optimization`` keyword enables a significant optimization of
the ``quantum_step`` function. This keyword can be set to True if the ``reject``
function is guaranteed to return the value ``reject(x)`` also on the non-algorithmic
subspace of x. For instance, if x = [0,0,1] in a depth 4 tree, the encoded state is
.. math::
\begin{align}
\ket{\text{branch_qa}} &= \ket{0}\ket{1}\ket{0}\ket{0}\\
\ket{\text{h}} &= \ket{1}\\
\ket{x} &= \ket{\text{branch_qa}}\ket{\text{h}}
\end{align}
A state from the non-algorithmic subspace of x is now a state that has non-zero
entries in ``branch_qa`` at indices less than ``h`` ie.
.. math::
\begin{align}
\ket{\text{branch_qa}_{NA}} &= \ket{1}\ket{1}\ket{0}\ket{0}\\
\ket{\text{h}} &= \ket{1}\\
\ket{\tilde{x}} &= \ket{\text{branch_qa}_{NA}}\ket{\text{h}}
\end{align}
For the ``subspace_optimization`` to return proper results, the ``reject``
function must therefore satisfy:
.. math::
\text{reject}(\ket{x}) = \text{reject}(\ket{\tilde{x}})
.. note::
Many implementations of backtracking also include the possibility for
deciding which entries of x to assign based on some user provided heuristic.
The quantum version also supports this feature, however it is not yet
implemented in Qrisp.
Parameters
----------
max_depth : integer
The depth of the backtracking tree.
branch_qv : QuantumVariable
A QuantumVariable representing the possible branches of each node.
accept : function
A function taking an instance of QuantumBacktrackingTree and returning
a QuantumBool, which is ``True``, if called on a satisfying node.
reject : function
A function taking an instance of QuantumBacktrackingTree and returning
a QuantumBool, which is ``True``, if a called on a node where the corresponding
branch should no longer be investigated.
subspace_optimization : bool, optional
If set to ``True``, a significant optimization of the ``quantum_step`` function
will be applied. The reject function has to fullfil a certain property
for this to yield the correct results. Please check the "Details on the
predicate functions" section for more information. The default is ``False``.
Attributes
----------
h : :ref:`QuantumFloat`
A one hot encoded integer representing the height of the node. The root
has ``h = max_depth``, it's children have ``h = max_depth-1`` etc.
branch_qa : :ref:`QuantumArray`
A QuantumArray representing the path from the root to the current node.
The qtype of this QuantumArray is what is been provided as ``branch_qv``.
Note that the state of this array is the reversed path, ie. a the node
with path ``[1,1,0,1]`` in a depth 7 tree has the state:
$\ket{0}\ket{0}\ket{0}\ket{1}\ket{0}\ket{1}\ket{1}$
States that have a non-zero value at entries indexed smaller than ``h``,
are considered non-algorithmic and will never be visited
(eg. h=3, branch_qa = $\ket{1}\ket{1}\ket{1}\ket{1}$).
qs : :ref:`QuantumSession`
The QuantumSession of the backtracking tree.
max_depth : int
An integer specifying the maximum depth of each node.
Examples
--------
**Checking for the existence of a solution**
Even though primary purpose of backtracking algorithms is to find a solution,
at the core, Montanaros algorithm only determines solution existence. This can
however still be leveraged into a solution finding algorithm.
To demonstrate the solution existence functionality, we search the binary
tree that consists only of nodes with alternating branching.
We accept if we find the node ``[0,0,1]`` (doesn't exist in this tree).
For this we first set up the reject condition.
::
from qrisp import *
@auto_uncompute
def reject(tree):
oddity = QuantumBool()
for i in range(tree.h.size):
if i%2:
cx(tree.h[i], oddity)
parity = QuantumBool()
for i in range(tree.branch_qa.size):
cx(tree.branch_qa[i], parity)
exclude_init = (tree.h < tree.max_depth-1)
return exclude_init & (oddity != parity)
This function determines first determines the oddity of the height parameter
(remember ``tree.h`` has one-hot encoding!). Next the parity of the branching
path is evaluated. Parity means "is the amount of ones in the path even or odd".
We will reject the node if the oddity of is unequal to the parity and therefore
reject any path that took a 1 after it already took a 1 (same for 0).
On the root and it's children there will be rejection to allow for two different
paths.
We now implement the accept condition:
::
@auto_uncompute
def accept(tree):
height_condition = (tree.h == 0)
path_condition = QuantumBool()
mcx(tree.branch_qa[::-1], path_condition, ctrl_state = "001")
return height_condition & path_condition
Subsequently we set up the class instance:
::
from qrisp.quantum_backtracking import QuantumBacktrackingTree
tree = QuantumBacktrackingTree(max_depth = 3,
branch_qv = QuantumFloat(1),
accept = accept,
reject = reject)
tree.init_node([])
We can evaluate the statevector:
>>> tree.statevector()
1.0*|[]>
The ``[]`` indicates that this is the root state. If the tree was in the state
of a child of the root (say the one connected to the 1 branch) it would be ``[1]``.
Note that the ``statevector`` method decodes the QuantumVariables holding the
node state for convenient readibility. If you want to see the encoded variables
you can take a look at the :ref:`QuantumSession` s :meth:`statevector method<qrisp.QuantumSession.statevector>`:
>>> tree.qs.statevector()
|0>**3*|3>
We can also visualize the statevector of the tree:
>>> import matplotlib.pyplot as plt
>>> tree.visualize_statevector()
>>> plt.show()
.. image:: ./root_state_plot.png
:width: 200
:alt: Root statevector plot
:align: left
|
|
|
|
|
And finally evaluate the algorithm:
::
qpe_res = tree.estimate_phase(precision = 4)
Perform a measurement
>>> mes_res = qpe_res.get_measurement()
>>> mes_res[0]
0.1036
The 0 component has only 10.36% probability of appearing, therefore we can conclude,
that in the specified tree no such node exists.
We now perform the same process but with a trivial reject function:
::
def reject(tree):
return QuantumBool()
tree = QuantumBacktrackingTree(max_depth = 3,
branch_qv = QuantumFloat(1),
accept = accept,
reject = reject)
tree.init_node([])
qpe_res = tree.estimate_phase(precision = 4)
>>> mes_res = qpe_res.get_measurement()
>>> mes_res[0]
0.5039
We see a probability of more than 50%, implying a solution exists in
this tree.
**Finding a solution**
Montanaros approach to determine a solution is to classically traverse the tree,
by always picking the child node where the quantum algorithm returns "Node exists".
Finding a solution can therefore be considered a hybrid algorithm.
To demonstrate, we search for the node ``[1,1,1]`` with a trivial reject function.
::
@auto_uncompute
def accept(tree):
height_condition = (tree.h == tree.max_depth - 3)
path_condition = QuantumBool()
mcx(tree.branch_qa[-3:], path_condition)
return height_condition & path_condition
def reject(tree):
return QuantumBool()
Set up the QuantumBacktrackingTree instance:
>>> max_depth = 4
>>> tree = QuantumBacktrackingTree(max_depth,
branch_qv = QuantumFloat(1),
accept = accept,
reject = reject)
And call the solution finding algorithm:
>>> tree.find_solution(precision = 5)
[1, 1, 1]
**Using the subspace_optimization keyword**
To demonstrate the usage of this feature, we create two tree instances - one
with and one without the optimization.
::
def accept(tree):
return QuantumBool()
def reject(tree):
return QuantumBool()
>>> opt_tree = QuantumBacktrackingTree(3, branch_qv = QuantumFloat(1), accept = accept, reject = reject, subspace_optimization = True)
>>> no_opt_tree = QuantumBacktrackingTree(3, branch_qv = QuantumFloat(1), accept = accept, reject = reject, subspace_optimization = False)
Perform a ``quantum_step`` on both of them:
>>> opt_tree.quantum_step()
>>> no_opt_tree.quantum_step()
And evaluate some benchmarks:
>>> no_opt_tree.qs.compile().depth()
89
>>> no_opt_tree.qs.compile().cnot_count()
68
With the optimization these values are much better:
>>> opt_tree.qs.compile().depth()
48
>>> opt_tree.qs.compile().cnot_count()
38
"""
def __init__(self, max_depth, branch_qv, accept, reject, subspace_optimization = False):
self.max_depth = max_depth
self.degree = 2**branch_qv.size
self.branch_qa = QuantumArray(qtype=branch_qv, shape=max_depth)
self.h = OHQInt(max_depth+1, name="h*", qs=self.branch_qa.qs)
self.qs = self.h.qs
self.accept_function = accept
self.reject_function = reject
self.subspace_optimization = subspace_optimization
def accept(self):
return self.accept_function(self)
def reject(self):
return self.reject_function(self)
@auto_uncompute
def qstep_diffuser(self, even, ctrl=[], min_height_assumption = 0):
"""
Performs the operators :math:`R_A` or :math:`R_B`. For more information on these operators check `the paper <https://arxiv.org/abs/1509.02374>`_.
Parameters
----------
even : bool
Depending on the parameter, the diffuser acts on the subspaces $\mathcal H_x=\{\ket{x}\}\cup\{\ket{y}\,|\,x\\rightarrow y\}$ where $x$ has odd (``even=False``) or even (``even=True``) height.
Note that "even" refers to the parity of the ``h`` attribute instead of the distance from the root.
If the ``max_depth`` of the tree is odd, and ``even=False`` then $R_A$ (otherwise $R_B$) is performed, and vice verse if the ``max_depth`` is even.
ctrl : List[Qubit], optional
A list of qubits that allows performant controlling. The default is [].
Examples
--------
We set up a QuantumBackTrackingTree and perform the diffuser on a marked node
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def reject(tree):
return QuantumBool()
@auto_uncompute
def accept(tree):
return (tree.h == 1)
tree = QuantumBacktrackingTree(3, QuantumFloat(
1, name = "branch_qf*"), accept, reject)
tree.init_node([1,1])
>>> print(tree.qs.statevector())
|0>*|1>**3
>>> tree.qstep_diffuser(even = False)
>>> print(tree.qs.statevector())
|0>*|1>**3
We see that the node (as expected) is invariant under :math:`R_A`.
"""
# This function performs the operation
# D_x = U_x (1 - (1+(-1)**accept(x))*|x><x|) U_x^(-1)
# For more information, check the beginning of this file
# Perform U_x^(-1)
with invert():
psi_prep(self, even=even, min_height_assumption = min_height_assumption)
# We now perform the operation
# 1 - (1+(-1)**accept(x))*|x><x|
# by executing an appropriate mcz gate
# An additional detail to consider:
# We allow for additional control qubits. This is important because
# this operator will undergo phase estimation and we want to prevent
# performing automatic synthesis of the controlled operation,
# as this would imply controlling EVERY gate. Instead we can just
# control the mcz gate.
# Prepare control state specificator
ctrl_state = ""
mcz_list = []
# D_x operates on the space span(|x>, {|y>, x->y})
# In order to make sure our mcz gate only marks |x>, we can use the
# oddity of h, because if h(x) is odd, then h(y) is not.
oddity_qbl = QuantumBool()
for i in range(self.max_depth+1):
if i < min_height_assumption:
continue
if bool(i%2) != even:
cx(self.h[i], oddity_qbl)
ctrl_state += "1"
mcz_list.append(oddity_qbl)
# Determine accept value
accept_value = self.accept()
mcz_list.append(accept_value)
ctrl_state += "0"
# Add additional control qubits
mcz_list += ctrl
ctrl_state += "1"*len(ctrl)
# Perform mcz gate
mcz(mcz_list, ctrl_state=ctrl_state)
# We now perform the phase-flip on the child states.
# For more details why this yields the correct behavior please consult
# the text at the beginning of this file.
# For this we first have to perform the lifting operation.
# Lifting means, that the child states |y> are mapped to their parent.
# The first step to achieve this is to swap the branch information
# into a temporary container. This way the branching information is 0.
# Check if |x> 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()
for i in range(self.max_depth):
if i < min_height_assumption or self.subspace_optimization:
continue
if bool(i%2) == even:
with control(self.h[i], ctrl_method = "gray_pt"):
swap(temporary_container, self.branch_qa[i])
# The second step is to increment h. Due to the one-hot encoding of h,
# we can do this for free with a compiler swap.
self.h.reg.insert(0, self.h.reg.pop(-1))
# Determine reject value
reject_value = self.reject_function(self)
mcz_list = [reject_value]
ctrl_state = "1"
# Make sure we only apply the phase to the child states (parent states
# have oddity 1)
mcz_list.append(oddity_qbl)
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"
# Add extra controls
mcz_list += ctrl
ctrl_state += "1"*len(ctrl)
#Perform MCZ gate
mcz(mcz_list, ctrl_state = ctrl_state)
#Reverse compiler swap
self.h.reg.append(self.h.reg.pop(0))
#Reintroduce branching information
for i in range(self.max_depth):
if i < min_height_assumption or self.subspace_optimization:
continue
if bool(i%2) == even:
with control(self.h[i], ctrl_method = "gray_pt_inv"):
swap(temporary_container, self.branch_qa[i])
#Delete temporary container.
temporary_container.delete()
# Perform U_x
psi_prep(self, even=even, min_height_assumption = min_height_assumption)
[docs]
def quantum_step(self, ctrl=[], min_height_assumption = 0):
"""
Performs the quantum step operator $R_BR_A$.
For more information check the :meth:`diffuser method <qrisp.quantum_backtracking.QuantumBacktrackingTree.qstep_diffuser>`.
Parameters
----------
ctrl : List[Qubit], optional
A list of qubits, the step operator should be controlled on. The default is [].
"""
self.qstep_diffuser(even=not self.max_depth % 2, ctrl=ctrl, min_height_assumption = min_height_assumption)
self.qstep_diffuser(even=self.max_depth % 2, ctrl=ctrl, min_height_assumption = min_height_assumption - 1)
[docs]
def estimate_phase(self, precision):
r"""
Performs :meth:`quantum phase estimation <qrisp.QPE>` on the :meth:`quantum step operator <qrisp.quantum_backtracking.QuantumBacktrackingTree.quantum_step>`.
If executed with sufficient precision, the phase estimation will yield a QuantumFloat, where the probability of the 0 component indicates the presence of a node where the ``accept`` function yielded ``True``.
If the probability is higher than 3/8 :math:`\Rightarrow` A solution exists.
If the probability is less than 1/4 :math:`\Rightarrow` No solution exists.
Otherwise :math:`\Rightarrow` Increase precision.
In general, the required precision is proportional to
.. math::
\frac{\text{log}_2(Tn)}{2} + \beta
Where :math:`T` is the amount of nodes, that would be visited by a classical algorithm, :math:`n` is the maximum depth and :math:`\beta` is a universal constant.
Parameters
----------
precision : int
The precision to perform the quantum phase estimation with.
Returns
-------
qpe_res : :ref:`QuantumFloat`
The QuantumFloat containing the result of the phase estimation.
"""
qpe_res = QuantumFloat(precision, -precision, qs = self.qs)
h(qpe_res)
from qrisp import check_if_fresh
if check_if_fresh(self.h.reg[:-1], self.qs):
height_tracker = int(self.max_depth) + 1
else:
height_tracker = -1
for i in range(qpe_res.size):
if height_tracker >= 0 and False:
for j in range(2**i):
self.quantum_step(ctrl=[qpe_res[i]], min_height_assumption = height_tracker)
height_tracker -= 2
else:
with IterationEnvironment(self.qs, 2**i, precompile=True):
self.quantum_step(ctrl=[qpe_res[i]])
QFT(qpe_res, inv=True)
return qpe_res
[docs]
def init_phi(self, path):
r"""
Initializes the normalized version of the state :math:`\ket{\phi}`.
.. math::
\ket{\phi} = \sqrt{n}\ket{r} + \sum_{x \neq r, \\ x \rightsquigarrow x_0} (-1)^{l(x)} \ket{x}
Where :math:`x \rightsquigarrow x_0` means that :math:`x` is on the path from :math:`r` to :math:`x_0` (including :math:`x_0`).
If :math:`x_0` is a marked node, this state is invariant under the quantum step operator.
Parameters
----------
path : List
The list of branches specifying the path from the root to :math:`x_0`.
Examples
--------
We set up a backtracking tree of depth 3, where the marked element is the 111 node.
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def reject(tree):
return QuantumBool()
@auto_uncompute
def accept(tree):
return (tree.branch_qa[0] == 1) & (tree.branch_qa[1] == 1) & (tree.branch_qa[2] == 1)
tree = QuantumBacktrackingTree(3, QuantumFloat(
1, name = "branch_qf*"), accept, reject)
Initialize :math:`\ket{\phi}` and evaluate the statevector:
>>> tree.init_phi([1,1,1])
>>> print(tree.qs.statevector())
(0.816496014595032*|0>*|1>**3 - 0.816496014595032*|0>**2*|1>*|2> + 1.0*sqrt(2)*|0>**3*|3> - 0.816496014595032*|1>**3*|0>)/2
Perform the quantum step and evaluate the statevector again:
>>> tree.quantum_step()
>>> print(tree.qs.statevector())
(0.816496014595032*|0>*|1>**3 - 0.816496014595032*|0>**2*|1>*|2> + 1.0*sqrt(2)*|0>**3*|3> - 0.816496014595032*|1>**3*|0>)/2
We see that the node (as expected) is invariant under the quantum step operator.
"""
h_state = {}
h_state[self.max_depth] = (self.max_depth)**0.5
for i in range(1, len(path)+1):
h_state[self.max_depth - i] = (-1)**(i)
self.h[:] = h_state
for i in range(1, len(path)+1):
with self.h == self.max_depth - i:
for j in range(i):
self.branch_qa[-j-1].encode(path[j], permit_dirtyness=True)
[docs]
def init_node(self, path):
"""
Initializes the state of a given node.
Parameters
----------
path : List
List of the branch labels indicating the path from the root to the node.
Examples
--------
We initialize a backtracking tree in the 101 node.
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def reject(tree):
return QuantumBool()
@auto_uncompute
def accept(tree):
return QuantumBool()
tree = QuantumBacktrackingTree(3, QuantumFloat(
1, name = "branch_qf*"), accept, reject)
tree.init_node([1,0,1])
"""
self.h[:] = self.max_depth - len(path)
if len(path):
self.branch_qa[-len(path):] = path[::-1]
[docs]
def subtree(self, new_root):
"""
Returns the subtree of a given node.
Parameters
----------
new_root : list
The path from the root of self to the root of the subtree.
Returns
-------
QuantumBacktrackingTree
The subtree starting at the specified root.
Examples
--------
We initiate a QuantumBacktrackingTree with trivial reject
function and create a subtree starting at an accepted node-
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def accept(tree):
height_cond = (tree.h == 2)
return height_cond
@auto_uncompute
def reject(tree):
return QuantumBool()
Create and initiate the parent tree.
>>> depth = 5
>>> tree = QuantumBacktrackingTree(depth, QuantumFloat(1, name = "branch_qf*"), accept, reject)
>>> tree.init_node([])
>>> print(accept(tree))
{False: 1.0}
We now create the subtree, where the new root has height two, ie. the accept
function returns ``True``.
>>> subtree = tree.subtree([0,1,0])
>>> subtree.init_node([])
>>> print(accept(subtree))
{True: 1.0}
"""
return Subtree(self, new_root)
[docs]
def copy(self):
"""
Returns a copy of self. Copy means a QuantumBacktrackingTree with identical
depth, accept/reject functions etc. but with freshly allocated QuantumVariables.
Returns
-------
QuantumBacktrackingTree
Another instance with the same depth/accept/reject etc.
"""
return Subtree(self, [])
[docs]
def find_solution(self, precision, cl_accept=None, measurement_kwargs={}):
"""
Determines a path to a solution.
Parameters
----------
precision : integer
The precision to perform the quantum phase estimation(s) with.
cl_accept : function, optional
A classical version of the accept function of self. Needs to
receive a list to indicate a path and returns a bool wether the
node is accepted. By default, the accept function of self will be
evaluated on a simulator.
measurement_kwargs : dictionary
A dictionary to give keyword arguments that specify how measurements
are evaluated. The default is {}.
Returns
-------
List
A list indicating the path to a node where the accept function
returns True.
Examples
--------
We create a accept function that marks the node [0,1] and a trivial
reject function.
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat, mcx
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def accept(tree):
height_cond = (tree.h == 1) # The [0,1] node has height 1
path_cond = QuantumBool()
mcx(list(tree.branch_qa)[1:], path_cond, ctrl_state="10")
return path_cond & height_cond
@auto_uncompute
def reject(tree):
return QuantumBool()
Create backtracking tree object:
>>> depth = 3
>>> tree = QuantumBacktrackingTree(depth, QuantumFloat(1, name = "branch_qf*"), accept, reject)
Find solution
>>> res = tree.find_solution(4)
>>> print(res)
[0, 1]
"""
return find_solution(self, precision, cl_accept, measurement_kwargs=measurement_kwargs)
[docs]
def path_decoder(self, h, branch_qa):
"""
Returns the path representation for a given constellation of the
``h`` and ``branch_qa`` variables. The path representation is a list
indicating which branches to take starting from the root.
This function exists because the encoding of the nodes is hardware
efficient but inconvenient for humans to read.
Parameters
----------
h : integer
The integer describing the height of the node.
branch_qa : list
The list of branches to take to reach the root, starting from the node.
Returns
-------
list
The list of path variables to take to reach the node, starting
from the root.
Examples
--------
We create a QuantumBacktrackingTree, initiate a node and retrieve the path.
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat, multi_measurement
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def accept(tree):
return QuantumBool()
@auto_uncompute
def reject(tree):
return QuantumBool()
>>> depth = 5
>>> tree = QuantumBacktrackingTree(depth, QuantumFloat(1, name = "branch_qf*"), accept, reject)
>>> tree.init_node([1,0])
>>> multi_measurement([tree.h, tree.branch_qa])
{(3, OutcomeArray([0, 0, 0, 0, 1])): 1.0}
Retrieve the path
>>> tree.path_decoder(3, [0, 0, 0, 0, 1])
[1, 0]
"""
l = self.max_depth - h
return list(branch_qa[::-1][:l])
[docs]
def statevector_graph(self, return_root=False):
r"""
Returns a NetworkX Graph representing the quantum state of the backtracking tree.
The nodes have an ``amplitude`` attribute, indicating the complex amplitude of that node.
Parameters
----------
return_root : bool, optional
If set to ``True``, this method will also return the root node. The default is False.
Returns
-------
networkx.DiGraph
A graph representing the statevector.
Examples
--------
We initialize a backtracking tree, initialize a :math:`\ket{\phi}` state and retrieve
the statevector graph.
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def reject(tree):
return QuantumBool()
@auto_uncompute
def accept(tree):
return (tree.branch_qa[0] == 1) & (tree.branch_qa[1] == 1) & (tree.branch_qa[2] == 1)
Create backtracking tree and initialize :math:`\ket{\phi}`.
>>> tree = QuantumBacktrackingTree(3, QuantumFloat(1, name = "branch_qf*"), accept, reject)
>>> tree.init_phi([1,1,1])
>>> statevector_graph = tree.statevector_graph()
>>> print(statevector_graph.nodes())
[QBTNode(path = [], amplitude = (0.7071066+0j)), QBTNode(path = [0], amplitude = 0j), QBTNode(path = [1], amplitude = (-0.408248-4.4703484e-08j)), QBTNode(path = [0, 0], amplitude = 0j), QBTNode(path = [0, 1], amplitude = 0j), QBTNode(path = [1, 0], amplitude = 0j), QBTNode(path = [1, 1], amplitude = (0.4082481+1.4901161e-08j)), QBTNode(path = [0, 0, 0], amplitude = 0j), QBTNode(path = [0, 0, 1], amplitude = 0j), QBTNode(path = [0, 1, 0], amplitude = 0j), QBTNode(path = [0, 1, 1], amplitude = 0j), QBTNode(path = [1, 0, 0], amplitude = 0j), QBTNode(path = [1, 0, 1], amplitude = 0j), QBTNode(path = [1, 1, 0], amplitude = 0j), QBTNode(path = [1, 1, 1], amplitude = (-0.40824816+5.9604645e-08j))]
>>> statevector_graph, root = tree.statevector_graph(return_root = True)
>>> print(root)
QBTNode(path = [], amplitude = (0.7071066+0j))
"""
from networkx import DiGraph
sv_function = self.qs.statevector("function")
res_graph = DiGraph()
root = QBTNode(self, [])
root.amplitude = sv_function(root.sv_specifier())
res_graph.add_node(root)
last_layer = [root]
for i in range(self.max_depth):
next_layer = []
for parent_node in last_layer:
for j in range(2**self.branch_qa[0].size):
child_node_path = list(parent_node.path) + [self.branch_qa[0].decoder(j)]
child_node = QBTNode(self, child_node_path)
child_node.amplitude = sv_function(child_node.sv_specifier())
res_graph.add_node(child_node)
res_graph.add_edge(parent_node, child_node,
label=child_node_path[-1])
next_layer.append(child_node)
last_layer = next_layer
if return_root:
return res_graph, root
return res_graph
[docs]
def visualize_statevector(self, pos=None):
"""
Visualizes the statevector graph.
Parameters
----------
pos : dict, optional
A dictionary indicating the positional layout of the nodes. For more information visit
`this page <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw.html>`_
By default is suitable will be generated.
Examples
--------
We initialize a backtracking tree and visualize:
::
from qrisp import auto_uncompute, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
import matplotlib.pyplot as plt
@auto_uncompute
def reject(tree):
return QuantumBool()
@auto_uncompute
def accept(tree):
return (tree.branch_qa[0] == 1) & (tree.branch_qa[1] == 1) & (tree.branch_qa[2] == 1)
>>> tree = QuantumBacktrackingTree(3, QuantumFloat(1, name = "branch_qf*"), accept, reject)
>>> tree.init_node([])
>>> tree.visualize_statevector()
>>> plt.show()
.. image:: ./root_state_plot.png
:width: 200
:alt: Root statevector plot
:align: left
>>> tree = tree.copy()
>>> tree.init_phi([1,1,1])
>>> tree.visualize_statevector()
.. image:: ./phi_state_plot.png
:width: 200
:alt: Root statevector plot
:align: left
"""
G, root = self.statevector_graph(return_root = True)
def tree_layout(G, node, depth, theta_parent, res_dic={}):
r = depth + 1
delta_theta = 2*np.pi/self.degree**(depth+1)
theta_start = theta_parent - 2*np.pi/self.degree**(depth)/4
children = list(G.neighbors(node))
for i in range(len(children)):
theta = theta_start + i*delta_theta
res_dic[children[i]] = (r*np.sin(theta), r*np.cos(theta))
tree_layout(G, children[i], depth+1, theta, res_dic)
return res_dic
pos= tree_layout(G, root, 0, 0)
pos[root]= (0, 0)
import colorsys
def complex_to_color(cnumber):
angle = np.angle(cnumber)
radius = np.abs(cnumber)
# Normalize the angle to the range [0, 2*pi)
angle= (angle + np.pi*5/2) % (2 * np.pi)
# Map the angle to the hue component of the color
hue = angle / (2*np.pi)
# Map the radius to the saturation and value components of the color
saturation = 1
value = 1
# Convert the HSV components to RGB
hsv_color = (hue, saturation, value)
rgb_color = colorsys.hsv_to_rgb(*hsv_color)
from scipy.special import expit
intensity = expit((radius-0.2)*10)
# Convert the RGB components to hexadecimal format
hex_color = '#{:02x}{:02x}{:02x}'.format(
int(rgb_color[0] * 255*intensity),
int(rgb_color[1] * 255*intensity),
int(rgb_color[2] * 255*intensity)
)
return hex_color
colors = [complex_to_color(node.amplitude) for node in G.nodes()]
nx.draw(G, pos)
nx.draw_networkx_nodes(G, pos, node_color=colors)
edge_labels = dict([((n1, n2), l)
for n1, n2, l in G.edges(data="label")])
nx.draw_networkx_edge_labels(G, pos=pos, edge_labels=edge_labels)
[docs]
def statevector(self):
"""
Returns a SymPy statevector object representing the state of the tree
with decoded node labels.
Returns
-------
state : sympy.Expr
A SymPy quantum state representing the statevector of the tree.
Examples
--------
We create a QuantumBacktrackingTree with and investigate the action of the
:meth:`quantum step diffuser <qrisp.quantum_backtracking.QuantumBacktrackingTree.qstep_diffuser>`
::
from qrisp import auto_uncompute, mcx, QuantumBool, QuantumFloat
from qrisp.quantum_backtracking import QuantumBacktrackingTree
@auto_uncompute
def accept(tree):
height_cond = (tree.h == 1) # The [0,1] node has height 1
path_cond = QuantumBool()
mcx(list(tree.branch_qa)[1:], path_cond, ctrl_state="10")
return path_cond & height_cond
@auto_uncompute
def reject(tree):
height_cond = (tree.h == 2) # The [1] node has height 2
path_cond = QuantumBool()
mcx(list(tree.branch_qa)[-1], path_cond, ctrl_state="1")
return path_cond & height_cond
Create tree and initialize a node where neither accept nor reject are True.
>>> tree = QuantumBacktrackingTree(3, QuantumFloat(1, name = "branch_qf*"), accept, reject)
>>> tree.init_node([0,0])
Evaluate statevector
>>> print(tree.statevector())
1.0*|[0, 0]>
>>> tree.qstep_diffuser(even = False)
>>> print(tree.statevector())
-0.666660010814667*|[0, 0, 0]> - 0.666660010814667*|[0, 0, 1]> + 0.333330005407333*|[0, 0]>
We see that the :meth:`quantum step diffuser <qrisp.quantum_backtracking.QuantumBacktrackingTree.qstep_diffuser>`
moves the state to the children of the [0,0] node (ie. [0,0,0] and [0,0,1]).
We now investigate how it behaves on nodes that are accepted/rejected:
Initiate a new tree
>>> tree = tree.copy()
>>> tree.init_node([0,1])
>>> tree.qstep_diffuser(even = False)
>>> tree.statevector()
1.0*|[0, 1]>
As expected, the accepted node is invariant.
To investigate the rejected node, we create another copy:
>>> tree = tree.copy()
>>> tree.init_node([1])
>>> tree.qstep_diffuser(even = True)
>>> tree.statevector()
-1*|[1]>
As expected, the node has eigenvalue -1.
If you are unsure why these statevectors are eigenvector please check
`the paper <https://arxiv.org/abs/1509.02374>`_.
"""
sv_function = self.qs.statevector("function", decimals = 10)
# Internal qvs are the quantum variables that specify a backtrackingtree node
# internal_qvs = [self.h, self.branch_workspace] + list(self.branch_qa)
internal_qvs = [self.h] + list(self.branch_qa)
# External qvs are any quantum variables that are also registered in the QuantumSession
# but don't specify a node
external_qvs = list(self.qs.qv_list)
# Remove the internal qvs from the external qv_list
for qv in internal_qvs:
for i in range(len(external_qvs)):
if hash(external_qvs[i]) == hash(qv):
external_qvs.pop(i)
break
# Get a list of possible labels for each qv
internal_qv_labels = []
for qv in internal_qvs:
label_list = []
for i in range(2**qv.size):
label_list.append(qv.decoder(i))
internal_qv_labels.append(label_list)
external_qv_labels = []
for qv in external_qvs:
label_list = []
for i in range(2**qv.size):
label_list.append(qv.decoder(i))
external_qv_labels.append(label_list)
# This retrieves a list of all possible constellations of labels
internal_label_product = list(product(*internal_qv_labels))
external_label_product = list(product(*external_qv_labels))
# This will be the sympy object that is returned
res_state = 0
# Go through all internal label consteallations (ie. node states)
for internal_label_const in internal_label_product[::-1]:
# print(internal_label_const)
# Get the path to that node state
path = self.path_decoder(internal_label_const[0], internal_label_const[1:])
# print(path)
# Create a label dic for the sv_function
internal_label_dic= {internal_qvs[i]: internal_label_const[i] for i in range(len(internal_qvs))}
# If there are no external qvs, we can simply call the sv_function
# with the label dic
if len(external_qvs) == 0:
amplitude = sv_function(internal_label_dic, 5)
if abs(amplitude) < 1E-5:
continue
# print(res_state)
# Add the corresponding ket
res_state += np.round(amplitude, 5) * OrthogonalKet(str(path))
# print(res_state)
# If there are external qvs we do a similar procedure to go through
# all label constellations
else:
for external_label_const in external_label_product:
# Set up the external label dic
external_label_dic= {external_qvs[i]: external_label_const[i] for i in range(len(external_qvs))}
# Integrate the internal label dic
external_label_dic.update(internal_label_dic)
# Retrieve the amplitude
amplitude = sv_function(external_label_dic)
if abs(amplitude) < 1E-5:
continue
external_ket_expr = 1
# Generate the ket expression for the external qvs
for label in external_label_const:
external_ket_expr *= OrthogonalKet(label)
# Add the corresponding state
res_state += amplitude * \
OrthogonalKet(str(path)) * external_ket_expr
return res_state
class OHQInt(QuantumVariable):
def decoder(self, i):
# One hot encoding:
# Red: [1,0,0,0]
# Green: [0,1,0,0]
# Yellow:[0,0,1,0]
# Blue: [0,0,0,1]
# [0,0,0,1][0,0,1,0]
# [0,0,0,0][1,0,0,1]
is_power_of_two = ((i & (i-1) == 0) and i != 0)
if is_power_of_two:
return int(np.log2(i))
else:
return -3
def __eq__(self, other):
if isinstance(other, int):
self.encoder(other)
eq_qbl = QuantumBool()
cx(self[other], eq_qbl)
return eq_qbl
else:
raise Exception(
f"Comparison with type {type(other)} not implemented")
__hash__ = QuantumVariable.__hash__
def is_even(self):
is_even = QuantumBool()
for i in range(self.size):
if not i % 2:
cx(self[i], is_even)
return is_even
def is_odd(self):
is_odd = QuantumBool()
for i in range(self.size):
if i % 2:
cx(self[i], is_odd)
return is_odd
def __lt__(self, other):
if isinstance(other, int):
less_than = QuantumBool()
for i in range(self.size):
if i < other:
cx(self[i], less_than)
return less_than
else:
raise Exception(f"Comparison for type {type(other)} not implemented")
def fan_in(control, target):
for qb in control:
cx(control, target)
"""
This function realizes the operator U_x, which has the property
U_x |x> = |psi_x>
For more details on these objects, check the details of the
paper (https://arxiv.org/abs/1509.02374)or the beginning of this file.
The general idea to implement this operator are the following two steps:
1. Manipulatre h such that |h> -> 1/N(|h> + c*|h-1>) with suitable N,c \in R
2. Manipulate branch_qa controlled on h-1 to bring the new branches into
into superposition.
The value of the constant c depends on wether x is the root or a terminal node.
For the root c = n**0.5*d_x**0.5
Otherwise c = d_x**0.5
Where d_x is the degree of the node.
"""
def psi_prep(x, even=True, min_height_assumption = 0):
# Determine c
c = x.degree**0.5
# The step |h> -> 1/N(|h> + c*|h-1>) will be performed by a ry gate
phase = np.arctan(c)*2
root_phase = np.arctan((c*np.sqrt(x.max_depth)))*2
rev_branch_qa = x.branch_qa
# N = x.max_depth+1
N = x.max_depth
# To achieve the first step, we use a circuit, that performs a similar function
# as a parametrized swap. That means:
# |00> ==> |00>
# |01> ==> sin(theta)*|01> + cos(theta)*|10>
# |10> ==> cos(theta)*|10> + sin(theta)*|01>
# |11> ==> |11>
#This way we can "move" the 1 of the one hot encoding up and down.
# After we moved the one, we apply an H-gate controlled on the target of the
# moevement, to set up the super position in the branch_qa
# Furthermore we also need to make sure that non-algorithmic states stay
# invariant under the U_x (otherwise they will also get tagged in qstep_diffuser).
# We achieve this by controlling the parameterized swap on the QuantumVariable
# which would be set to super position by the controlled H-gate.
# This QuantumVariable represents the branch information of the child states
# of H_x = <{|y> | |x> -> |y>} U {|x>} >.
# If it is in a non-zero state but the height variable indicates the parent
# state, the state is invariant.
if bool(N % 2) != even:
c_iswap_reduced(root_phase, rev_branch_qa[N-1], x.h[N-1], x.h[N])
x.qs.append(ch_gate, [x.h[N-1], rev_branch_qa[N-1]])
for i in range(int(even), N-1, 2):
if i + 1 < min_height_assumption:
continue
c_iswap_reduced(phase, rev_branch_qa[i], x.h[i], x.h[i+1])
x.qs.append(ch_gate, [x.h[i], rev_branch_qa[i]])
ch_gate = HGate().control()
# This circuit is a slightly modified (and controlled) version of the XXPlusYY gate
# https://qiskit.org/documentation/stubs/qiskit.circuit.library.XXPlusYYGate.html
def c_iswap(phi, ctrl, target_1, target_0):
h(target_1)
cx(target_1, target_0)
x(ctrl)
ctrl.qs().append(RYGate(-phi/2).control(), [ctrl, target_0])
ctrl.qs().append(RYGate(-phi/2).control(), [ctrl, target_1])
x(ctrl)
cx(target_1, target_0)
h(target_1)
# This circuit performs a similar function as the previous one but has a different
# behavior on |11> and also requires less resources.
# Since the |11> behavior is irrelevant we can also use this circuit.
def c_iswap_reduced(phi, ctrl, target_0, target_1):
phi = -phi/4 + np.pi/4
h(target_1)
cx(target_1, target_0)
x(ctrl)
ry(phi, target_0)
ry(phi, target_1)
#Usually we would now execute two controlled H-gates
#If there are multiple controls, we'd have to perform
#the corresponding mcx gate twice (to have two multi controlled H gates)
# ctrl.qs().append(ch_gate, [ctrl, target_0])
# ctrl.qs().append(ch_gate, [ctrl, target_1])
#Two prevent this situation we execute a circuit with the same semantics but
#only a single mcx gate
#--------------------------
#These one qubit gates make sure that the cx gates are acting as a controlled
#H-Gate
s([target_0, target_1])
h([target_0, target_1])
t([target_0, target_1])
if len(ctrl) == 1:
cx(ctrl, target_0)
cx(ctrl, target_1)
else:
cx(target_0, target_1)
mcx(ctrl, target_0)
cx(target_0, target_1)
t_dg([target_0, target_1])
h([target_0, target_1])
s_dg([target_0, target_1])
#----------------------------
ry(-phi, target_0)
ry(-phi, target_1)
x(ctrl)
cx(target_1, target_0)
h(target_1)
class Subtree(QuantumBacktrackingTree):
def __init__(self, parent_tree, root_path):
if len(root_path) > parent_tree.max_depth:
raise Exception(
"Tried to initialise subtree with root path longer than maximum depth")
QuantumBacktrackingTree.__init__(self,
parent_tree.max_depth,
parent_tree.branch_qa[0],
parent_tree.accept_function,
parent_tree.reject_function,
parent_tree.subspace_optimization
)
self.max_depth = parent_tree.max_depth - len(root_path)
self.root_path = root_path
self.original_tree = parent_tree
def init_node(self, path):
self.h[:] = self.max_depth - len(path)
path = self.root_path + path
if len(path):
self.branch_qa[-len(path):] = path[::-1]
def init_phi(self, path):
h_state = {}
h_state[self.max_depth] = (self.max_depth)**0.5
for i in range(1, len(path)+1):
h_state[self.max_depth - i] = (-1)**(i)
self.h[:] = h_state
rev_branch_qa = self.branch_qa[::-1]
with self.h == self.max_depth:
for k in range(len(self.root_path)):
rev_branch_qa[k][:] = self.root_path[k]
for i in range(1, len(path)+1):
with self.h == self.max_depth - i:
for j in range(i):
rev_branch_qa[j].encode(
path[j], permit_dirtyness=True)
for k in range(len(self.root_path)):
rev_branch_qa[k+i][:] = self.root_path[k]
def subtree(self, path):
return self.original_tree.subtree(path)
def find_solution(tree, precision, cl_accept=None, traversed_nodes=None, measurement_kwargs={}):
# The idea of this function is to use the quantum algorithm to check wether
# a the subtree of a given node contains a solution and then recursively call
# this function on that subtree.
# If there is no classical accept function given, we create a copy of the original
# tree and evaluate the quantum accept function on that node via the simulator
if cl_accept is None:
def cl_accept(path):
if isinstance(tree, Subtree):
copied_tree = tree.original_tree.copy()
else:
copied_tree = tree.copy()
copied_tree.init_node(path)
accept_qbl = copied_tree.accept()
mes_res = accept_qbl.get_measurement()
return mes_res == {True: 1}
# The first step is to check wether the current root is a solution
if isinstance(tree, Subtree):
path = tree.root_path
else:
path = []
if cl_accept(path):
return path
elif tree.max_depth == 0:
return None
# This list keeps track of which nodes have already been checked for solutions
if traversed_nodes is None:
traversed_nodes = []
# Initialize the root node
tree.init_node([])
# Perform quantum phase estimation
qpe_res = tree.estimate_phase(precision)
# Retrieve the measurement results
mes_res = multi_measurement([qpe_res, tree.h, tree.branch_qa], **measurement_kwargs)
# We will first check wether there is a solution
# The s variable will contain the probability to measure
# the qpe_res == 0 branch.
s = 0
# This list will contain the possible branches
new_branches = []
for k, v in mes_res.items():
# k[0] is the value of qpe_res
if k[0] == 0:
# If the measurement result is part of the qpe == 0 branch, add the
# probability to s
s += v
new_branches.append(k)
# If the probability is less then 0.25, there is no solution
if s <= 0.25:
return None
# If the probability is between 0.25 and 0.375, the qpe needs more precision
if s <= 0.375:
raise Exception(
"Executed find solution method of quantum backtracking algorithm with insufficient precision")
# To find the next node to check we will use a heuristic.
# After measurement of the 0 branch, the tree is collapsed to a state which
# also contains the |phi> state. The phi state is a superposition of nodes
# leading to the desired solution.
# We assume that the outcome with the smallest tree.h value is the state
# corresponding to |phi>. This proved to be the case in every situation we tested.
# If this assumption for whichever reason is not correct, the solution will still
# be found because of the recursive nature of this algorithm.
# Sort for the value of tree.h
new_branches.sort(key=lambda x: x[1])
for b in new_branches:
# Get the path to the new node
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])
# 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
solution=find_solution(subtree, precision, cl_accept, traversed_nodes, measurement_kwargs=measurement_kwargs)
# Leave loop if solution was found
if solution is not None:
break
else:
traversed_nodes.append(tuple(new_path))
else:
raise Exception(
"Executed find solution method of quantum backtracking algorithm with insufficient precision")
return solution
class QBTNode:
def __init__(self, tree, path, amplitude=None):
self.h=tree.max_depth - len(path)
self.path=path
self.tree=tree
self.amplitude=amplitude
def __hash__(self):
return hash(str(self.path))
def sv_specifier(self):
amplitude_state_specifyer={
self.tree.h: self.tree.max_depth - len(self.path)}
path=list(self.path)
if isinstance(self.tree, Subtree):
path=self.tree.root_path + path
for k in range(len(self.tree.branch_qa)):
if k < len(path):
amplitude_state_specifyer[self.tree.branch_qa[-1-k]]=path[k]
else:
amplitude_state_specifyer[self.tree.branch_qa[-1-k]]=0
return amplitude_state_specifyer
def __str__(self):
return "QBTNode(path = " + str(self.path) + ", amplitude = " + str(self.amplitude) + ")"
def __repr__(self):
return str(self)