From d7b9ddba474be0a1e9abdc72f4fa68d52cd1d82f Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Fri, 8 Sep 2023 21:52:20 +0900 Subject: [PATCH] Integrate QRAO into Qiskit Optimization (#487) * add qrao files * support primitives and remove opflow * update qrao * update qrao * add expecation_values getter * inherit OptimizationAlgorithm * add unittests for encoding * add unittests for optimizer * add unittests for optimizer Co-authored-by: Jim Garrison Co-authored-by: Bryce Fuller Co-authored-by: Jennifer Glick Co-authored-by: Caleb Johnson Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Co-authored-by: Toshinari Itoko Co-authored-by: Areeq Hasan * add reno and unittests for magic rounding * clean up * remove a unnecessary file * update the code * add tutorial and update the code * update pylintdict * fix lint * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update qiskit_optimization/algorithms/qrao/magic_rounding.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update releasenotes/notes/qrao-89d5ff1d2927de64.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * update the code * add explanations and reflect comments * update codes * fix Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * update the code * fix lint * Fix docs so they build * fix lint * fix spell * fix spell * fix spell * test * test * update * fix lint * use assertalmost equal in unittest * update * fix * rerun tutorial * add unittests for max per qubit= 2 and 1 * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * fix docs * lint * add unittest quadratic objective * update optimizer unittest * replaces rustworkx with networkx - updates type cast * minor updates of explanation * fix docstrings * update an error message * update error messages * fix lint * update tutorial and qiskit_algorithms * update * fix lint * fix * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Update docs/tutorials/13_quantum_random_access_optimizer.ipynb Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * fix * Update qiskit_optimization/algorithms/qrao/__init__.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Update releasenotes/notes/qrao-89d5ff1d2927de64.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Update test/algorithms/qrao/test_magic_rounding.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Update test/algorithms/qrao/test_quantum_random_access_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * fix mypy * fix * fix * change the index number --------- Co-authored-by: Jim Garrison Co-authored-by: Bryce Fuller Co-authored-by: Jennifer Glick Co-authored-by: Caleb Johnson Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Co-authored-by: Toshinari Itoko Co-authored-by: Areeq Hasan Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: woodsp-ibm Co-authored-by: Takashi Imamichi --- .pylintdict | 13 + .../qiskit_optimization.algorithms.qrao.rst | 6 + .../aux_files/magic_state_rounding.svg | 658 +++++++++++++ docs/explanations/index.rst | 23 + docs/explanations/qrao.rst | 373 ++++++++ docs/index.rst | 1 + .../12_quantum_random_access_optimizer.ipynb | 892 ++++++++++++++++++ qiskit_optimization/algorithms/__init__.py | 8 + .../algorithms/optimization_algorithm.py | 27 +- .../algorithms/qrao/__init__.py | 141 +++ .../qrao/encoding_commutation_verifier.py | 70 ++ .../algorithms/qrao/magic_rounding.py | 473 ++++++++++ .../qrao/quantum_random_access_encoding.py | 578 ++++++++++++ .../qrao/quantum_random_access_optimizer.py | 313 ++++++ .../algorithms/qrao/rounding_common.py | 63 ++ .../qrao/semideterministic_rounding.py | 80 ++ releasenotes/notes/qrao-89d5ff1d2927de64.yaml | 62 ++ test/algorithms/qrao/__init__.py | 11 + test/algorithms/qrao/test_magic_rounding.py | 338 +++++++ .../test_quantum_random_access_encoding.py | 331 +++++++ .../test_quantum_random_access_optimizer.py | 195 ++++ .../qrao/test_semideterministic_rounding.py | 55 ++ 22 files changed, 4697 insertions(+), 14 deletions(-) create mode 100644 docs/apidocs/qiskit_optimization.algorithms.qrao.rst create mode 100644 docs/explanations/aux_files/magic_state_rounding.svg create mode 100644 docs/explanations/index.rst create mode 100644 docs/explanations/qrao.rst create mode 100644 docs/tutorials/12_quantum_random_access_optimizer.ipynb create mode 100644 qiskit_optimization/algorithms/qrao/__init__.py create mode 100644 qiskit_optimization/algorithms/qrao/encoding_commutation_verifier.py create mode 100644 qiskit_optimization/algorithms/qrao/magic_rounding.py create mode 100644 qiskit_optimization/algorithms/qrao/quantum_random_access_encoding.py create mode 100644 qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py create mode 100644 qiskit_optimization/algorithms/qrao/rounding_common.py create mode 100644 qiskit_optimization/algorithms/qrao/semideterministic_rounding.py create mode 100644 releasenotes/notes/qrao-89d5ff1d2927de64.yaml create mode 100644 test/algorithms/qrao/__init__.py create mode 100644 test/algorithms/qrao/test_magic_rounding.py create mode 100644 test/algorithms/qrao/test_quantum_random_access_encoding.py create mode 100644 test/algorithms/qrao/test_quantum_random_access_optimizer.py create mode 100644 test/algorithms/qrao/test_semideterministic_rounding.py diff --git a/.pylintdict b/.pylintdict index 9b77fe911..9230a0687 100644 --- a/.pylintdict +++ b/.pylintdict @@ -10,6 +10,7 @@ apidocs applegate args arxiv +atol autosummary backend backends @@ -54,6 +55,7 @@ eigen eigensolver eigensolvers eigenstate +embeddings entangler enum eq @@ -83,6 +85,7 @@ hamilton hamiltonian hamiltonians hastings +hayashi hoyer https ibm @@ -111,6 +114,7 @@ lp lucas macos makefile +masahito matplotlib maxcut maxfun @@ -119,6 +123,8 @@ mdl milp minimizer minimumeigenoptimizer +mmp +mpm multiset mypy nannicini @@ -133,6 +139,7 @@ np num numpy numpyminimumeigensolver +observables october opflow optimality @@ -146,8 +153,10 @@ parikh pauli paulis peleato +pmm pooya pos +ppp pre preprint prepend @@ -163,6 +172,8 @@ qiskit qiskit's qn qp +qrac +qrao quadratically quadraticconstraint quadraticobjective @@ -197,6 +208,7 @@ spsa src statevector stdout +stephen str subcollection subgraph @@ -232,6 +244,7 @@ xixj wavefunction wecker whitespace +wiesner williamson xs ys diff --git a/docs/apidocs/qiskit_optimization.algorithms.qrao.rst b/docs/apidocs/qiskit_optimization.algorithms.qrao.rst new file mode 100644 index 000000000..8ffb69f45 --- /dev/null +++ b/docs/apidocs/qiskit_optimization.algorithms.qrao.rst @@ -0,0 +1,6 @@ +.. _qiskit_optimization-algorithms-qrao: + +.. automodule:: qiskit_optimization.algorithms.qrao + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/explanations/aux_files/magic_state_rounding.svg b/docs/explanations/aux_files/magic_state_rounding.svg new file mode 100644 index 000000000..806fcf8ef --- /dev/null +++ b/docs/explanations/aux_files/magic_state_rounding.svg @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/explanations/index.rst b/docs/explanations/index.rst new file mode 100644 index 000000000..f7a7955a5 --- /dev/null +++ b/docs/explanations/index.rst @@ -0,0 +1,23 @@ +################################ +Qiskit Optimization Explanations +################################ + +This section of the documentation provides background and explanation around techniques, methods +etc. both useful with and used by in Qiskit Optimization. + +Explanations... +--------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + * + +| + +.. Hiding - Indices and tables + :ref:`genindex` + :ref:`modindex` + :ref:`search` + diff --git a/docs/explanations/qrao.rst b/docs/explanations/qrao.rst new file mode 100644 index 000000000..a06785b2a --- /dev/null +++ b/docs/explanations/qrao.rst @@ -0,0 +1,373 @@ +Background on Quantum Random Access Optimization: *Quantum relaxations, quantum random access codes, rounding schemes* +====================================================================================================================== + +This material provides a deeper look into the concepts behind Quantum +Random Access Optimization. + +Relaxations +----------- + +Consider a binary optimization problem defined on binary variables +:math:`m_i \in \{-1,1\}`. The choice of using :math:`\pm 1` variables +instead of :math:`0/1` variables is not important, but will be +convenient in terms of notation when we begin to re-cast this problem in +terms of quantum observables. We will be primarily interested in +`quadratic unconstrained binary optimization +(QUBO) `__ +problems, although the ideas in this document can readily extend to +problems with more than quadratic terms, and problems with non-binary or +constrained variables can often be recast as a QUBO (though this +conversion will incur some overhead). + +Within mathematical optimization, +`relaxation `__ +is the strategy of taking some hard problem and mapping it onto a +similar version of that problem which is (usually) easier to solve. The +core idea here is that for useful relaxations, the solution to the +relaxed problem can give information about the original problem and +allow one to heuristically find better solutions. An example of +relaxation could be something as simple as taking a discrete +optimization problem and allowing a solver to optimize the problem using +continuous variables. Once a solution is obtained for the relaxed +problem, the solver must find a strategy for extracting a discrete +solution from the relaxed solution of continuous values. This process of +mapping the relaxed solution back onto original problem’s set of +admissible solutions is often referred to as **rounding**. + +For a concrete example of relaxation and rounding, see the +`Goemans-Williamson Algorithm for +MaxCut `__. + +Without loss of generality, the rest of this document will consider a +`MaxCut `__ objective +function defined on a graph :math:`G = (V,E)`. Our goal is to find a +partitioning of our vertices :math:`V` into two sets (:math:`+1` and +:math:`-1`), such that we maximize the number of edges which connect +both sets. More concretely, each :math:`v_i \in V` will be assigned a +binary variable :math:`m_i \in \{-1, 1\}`, and we will define the *cut* +of a variable assignment as: + +.. math:: \text{cut}(m) = \sum_{ij; e_{ij} \in E} \frac{1}{2}(1-m_i m_j) + +Quantum Relaxation +------------------ + +Our goal is to define a relaxation of our MaxCut objective function. We +will do this by mapping our objective function’s binary variables into +the space of single qubit Pauli observables and by embedding the set of +feasible inputs to cut(:math:`m`) onto the space of single-qubit quantum +product states. Let us denote this embedding :math:`F` as: + +.. math:: F: \{-1,1\}^{M} \mapsto \mathcal{D}(\mathbb{C}^{2^n}), + +.. math:: \text{cut}(m) \mapsto \text{Tr}\big(H\cdot F(m)\big), + +where :math:`M = |V|`, and :math:`H` is a quantum Hamiltonian which +encodes our objective function. + +For this to be `a valid +relaxation `__ +of our problem, it must be the case that: + +.. math:: \text{cut}(m) \geq \text{Tr}\big(H\cdot F(m)\big)\qquad \forall m \in \{-1,1\}^M. + +In order to guarantee this is true, we will enforce the stronger +condition that our relaxation **commutes** with our objective function. +In other words, cut(:math:`m`) is equal to the relaxed objective +function for all :math:`m \in \{-1,1\}^M`, rather than simply upper +bounding it. This detail will become crucially important further down +when we explicitly define our quantum relaxation. + +A Simple Quantum Relaxation +--------------------------- + +Before explicating the full quantum relaxation scheme based on +single-qubit Quantum Random Access Codes (QRACs), it may be helpful to +first discuss `a version of quantum +optimization `__ +which users may be more familiar with, but discussed in the language of +quantum relaxation and rounding. + +Consider the embedding + +.. math:: F^{(1)}: m \in \{-1,1\}^M \mapsto \{|0\rangle,|1\rangle\}^{\otimes M}, + +.. math:: \text{cut}(m) \mapsto \text{Tr}\big(H^{(1)}F^{(1)}(m)\big),\quad H^{(1)} = \sum_{ij; e_{ij} \in E} \frac{1}{2}(1-Z_i Z_j), + +where :math:`Z_i` indicates the single qubit Pauli-Z observable defined +on the :math:`i`\ ’th qubit and identity terms on all other qubits. It +is worth convincing yourself that this transformation is a valid +relaxation of our problem. In particular: + +.. math:: \text{cut}(m) = \text{Tr}\big(H^{(1)}F^{(1)}(m)\big) \quad \forall m \in \{-1,1\}^M + +This sort of embedding is currently used by many near-term quantum +optimization algorithms, including many `QAOA and VQE based +approaches `__. +Observe how although the relaxed version of our problem can exactly +reproduce the objective function cut(:math:`m`) for inputs of the form +:math:`\{|0\rangle,|1\rangle\}^{\otimes M}`, we are also free to +evaluate :math:`H^{(1)}` using a continuous superposition of such +states. This stands in analogy to how one might classically relax an +optimization problem such that they optimize the objective function +using continuous values. + +Crucially, a relaxation is only useful if there is some practical way to +**round** relaxed solutions back onto the original problem’s set of +admissible solutions. For this particular quantum relaxation, the +rounding scheme is simply given by measuring each qubit of our relaxed +solution in the :math:`Z`-basis. Measurement will project any quantum +state onto the set of computational basis states, and consequently, onto +the image of :math:`F^{(1)}`. + +Quantum Relaxations via Quantum Random Access Codes (QRACs) +----------------------------------------------------------- + +Quantum Random Access Codes were `first outlined in 1983 by Stephen +Wiesner +[2] `__ +and were used in the context of communication complexity theory. We will +not be using QRACs in the way they were originally conceived, instead we +are co-opting them to define our quantum relaxations. For this reason +will not provide a full introduction to RACs or QRACs, but encourage +interested readers to seek out more information about them. + +:math:`(1,1,1)`, :math:`(2,1,p)`, and :math:`(3,1,p)` Quantum Random Access Codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A :math:`(k,1,p)`-QRAC, is a scheme for embedding :math:`k` classical +bits into a 1-qubit state, such that given a single copy of this state, +you can recover any one of the :math:`k`-bits with probability :math:`p` +by performing some measurement. The simple quantum relaxation discussed +in the previous section is an example of a trivial :math:`(1,1,1)`-QRAC. +For convenience, we will write the :math:`(2,1,0.854)` and +:math:`(3,1,0.789)` QRACs as :math:`(2,1,p)` and :math:`(3,1,p)`, +respectively. It is worth noting :math:`(4, 1, p)`-QRAC :math:`(p > 1/2)` +has been `proven to be impossible. +[3] `__ + +As we generalize the simple example above, it will be helpful to write +out single qubit states decomposed in the Hermitian basis of Pauli +observables. + +.. math:: \rho = \frac{1}{2}\left(I + aX + bY + cZ \right),\quad |a|^2 + |b|^2 + |c|^2 = 1 + +The embeddings :math:`F^{(1)}`, :math:`F^{(2)}`, and :math:`F^{(3)}` +associated respectively with the :math:`(1,1,1), (2,1,p),` and +:math:`(3,1,p)` QRACs can now be written as follows: + +.. math:: + + \begin{array}{l|ll} + \text{QRAC} & &\text{Embedding into } \rho = \vert \psi(m)\rangle\langle\psi(m)\vert \\ + \hline + (1,1,1)&F^{(1)}(m): \{-1,1\} &\mapsto\ \vert\psi^{(1)}_m\rangle \langle\psi^{(1)}_m\vert = \frac{1}{2}\Big(I + {m_0}Z \Big) \\ + (2,1,p)&F^{(2)}(m): \{-1,1\}^2 &\mapsto\ \vert\psi^{(2)}_m\rangle \langle\psi^{(2)}_m\vert = \frac{1}{2}\left(I + \frac{1}{\sqrt{2}}\big({m_0}X+ {m_1}Z \big)\right) \\ + (3,1,p)&F^{(3)}(m): \{-1,1\}^3 &\mapsto\ \vert\psi^{(3)}_m\rangle \langle\psi^{(3)}_m\vert = \frac{1}{2}\left(I + \frac{1}{\sqrt{3}}\big({m_0}X+ {m_1}Y + {m_2}Z\big)\right) \\ + \end{array} + +.. math:: \text{Table 1: QRAC states} + +Note that for when using a :math:`(k,1,p)`-QRAC with bit strings +:math:`m \in \{-1,1\}^M, M > k`, these embeddings scale naturally via +composition by tensor product. + +.. math:: m \in \{-1,1\}^6,\quad F^{(3)}(m) = F^{(3)}(m_0,m_1,m_2)\otimes F^{(3)}(m_3,m_4,m_5) + +Similarly, when :math:`k \nmid M`, we can simply pad our input bitstring +with the appropriate number of :math:`+1` values. + +.. math:: m \in \{-1,1\}^4,\quad F^{(3)}(m) = F^{(3)}(m_0,m_1,m_2)\otimes F^{(3)}(m_3,+1,+1) + +Recovering Encoded Bits +~~~~~~~~~~~~~~~~~~~~~~~ + +Given a QRAC state, we can recover the values of the encoded bits by +estimating the expectation value of each bit’s corresponding observable. +Note that there is a re-scaling factor which depends on the density of +the QRAC. + +.. math:: + + \begin{array}{l|l|l|l} + \text{Embedding} & m_0 & m_1 & m_2\\ + \hline + \rho = F^{(1)}(m_0) &\text{Tr}\big(\rho Z\big) & & \\ + \rho = F^{(2)}(m_0,m_1) &\sqrt{2}\cdot\text{Tr}\big(\rho X\big) &\sqrt{2}\cdot\text{Tr}\big(\rho Z\big) & \\ + \rho = F^{(3)}(m_0,m_1,m_2) & \sqrt{3}\cdot\text{Tr}\big(\rho X\big) & \sqrt{3}\cdot\text{Tr}\big(\rho Y\big) & \sqrt{3}\cdot\text{Tr}\big(\rho Z\big) + \end{array} + +.. math:: \text{Table 2: Bit recovery from QRAC states} + +Encoded Problem Hamiltonians +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using the tools we have outlined above, we can explicitly write out the +Hamiltonians which encode the relaxed versions of our MaxCut problem. We +do this by substituting each decision variable with the unique +observable that has been assigned to that variable under the embedding +:math:`F`. + +.. math:: + + \begin{array}{l|ll} + \text{QRAC} & \text{Problem Hamiltonian}\\ + \hline + (1,1,1)&H^{(1)} = \sum_{ij; e_{ij} \in E} \frac{1}{2}(1-Z_i Z_j)\\ + (2,1,p)&H^{(2)} = \sum_{ij; e_{ij} \in E} \frac{1}{2}(1-2\cdot P_{[i]} P_{[j]}),\quad P_{[i]} \in \{X,Z\}\\ + (3,1,p)&H^{(3)} = \sum_{ij; e_{ij} \in E} \frac{1}{2}(1-3\cdot P_{[i]} P_{[j]}),\quad P_{[i]} \in \{X,Y,Z\}\\ + \end{array} + +.. math:: \text{Table 3: Relaxed MaxCut Hamiltonians after QRAC embedding} + +Note that here, :math:`P_{[i]}` indicates a single-qubit Pauli +observable corresponding to decision variable :math:`i`. The bracketed +index here is to make clear that :math:`P_{[i]}` will not necessarily be +acting on qubit :math:`i`, because the :math:`(2,1,p)` and +:math:`(3,1,p)` no longer have a 1:1 relationship between qubits and +decision variables. + +Commutation of Quantum Relaxation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Note that for the :math:`(2,1,p)` and :math:`(3,1,p)` QRACs, we are +associating multiple decision variables to each qubit. This means that +each decision variable is assigned a *unique* single-qubit Pauli +observable and some subsets of these Pauli observables will be defined +on the same qubits. This can potentially pose a problem when trying to +ensure the commutativity condition discussed earlier + +Observe that under the :math:`(3,1,p)`-QRAC, any term in our objective +function of the form :math:`(1 - x_i x_j)` will map to a Hamiltonian +term of the form :math:`(1-3\cdot P_{[i]} P_{[j]})`. If both +:math:`P_{[i]}` and :math:`P_{[j]}` are acting on different qubits, +then :math:`P_{[i]}\cdot P_{[j]} = P_{[i]}\otimes P_{[j]}` and this term +of our Hamiltonian will behave as we expect. + +If however, :math:`P_{[i]}` and :math:`P_{[j]}` are acting on the same +qubit, the two Paulis will compose directly. Recall that the Pauli +matrices form a group and are self-inverse, thus we can deduce that the +product of two distinct Paulis will yield another element of the group +and it will not be the identity. + +Practically, this means that our commutation relationship will break and +:math:`\text{cut}(m) \not= \text{Tr}\big(H^{(1)}F^{(3)}(m)\big)` + +In order to restore the commutation of our encoding with our objective +function, we must introduce an additional constraint on the form of our +problem Hamiltonian. Specifically, we must guarantee that decision +variables which share an edge in our input graph :math:`G` are not +assigned to the same qubit under our embedding :math:`F` + +.. math:: \forall e_{ij} \in E,\quad F^{(3)}(\dots,m_i,\dots,m_j,\dots) = F^{(3)}(\dots,m_i,\dots)\otimes F^{(3)}(\dots,m_j,\dots) + +In [1] this is accomplished by finding a coloring of the graph G such +that no vertices with the same color share an edge, and then assigning +variables to the same qubit only if they have the same color. + +Quantum Rounding Schemes +------------------------ + +Because the final solution we obtain for the relaxed problem +:math:`\rho_\text{relax}` is unlikely to be in the image of :math:`F`, +we need a strategy for mapping :math:`\rho_\text{relax}` to the image of +:math:`F` so that we may extract a solution to our original problem. + +In [1] there are two strategies proposed for rounding +:math:`\rho_\text{relax}` back to :math:`m \in \{-1,1\}^M`. + +Semi-deterministic Rounding +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A natural choice for extracting a solution is to use the results of +Table :math:`2` and simply estimate +:math:`\text{Tr}(P_{[i]}\rho_\text{relax})` for all :math:`i` in order +to assign a value to each variable :math:`m_i`. The procedure described +in Table :math:`2` was intended for use on states in the image of +:math:`F`, however, we are now applying it to arbitrary input states. +The practical consequence is we will no longer measure a value close to +{:math:`\pm 1`}, {:math:`\pm \sqrt{2}`}, or {:math:`\pm \sqrt{3}`}, as +we would expect for the :math:`(1,1,1)`, :math:`(2,1,p)`, and +:math:`(3,1,p)` QRACs, respectively. + +We handle this by returning the sign of the expectation value, leading +to the following rounding scheme. + +.. math:: + + m_i = \left\{\begin{array}{rl} + +1 & \text{Tr}(P_{[i]}\rho_\text{relax}) > 0 \\ + X \sim\{-1,1\} & \text{Tr}(P_{[i]}\rho_\text{relax}) = 0 \\ + -1 & \text{Tr}(P_{[i]}\rho_\text{relax}) < 0 + \end{array}\right. + +Where :math:`X` is a random variable which returns either -1 or 1 with +equal probability. + +Notice that semi-deterministic rounding will faithfully recover :math:`m` +from :math:`F(m)` with a failure probability that decreases +exponentially with the number of shots used to estimate each +:math:`\text{Tr}(P_{[i]}\rho_\text{relax})` + +Magic State Rounding +~~~~~~~~~~~~~~~~~~~~ + +.. figure:: aux_files/magic_state_rounding.svg + :align: center + :width: 100% + + Three different encodings, the states and the measurement bases, of variables into a + single qubit. (a) One variable per qubit. (b) Two variables per qubit. (c) Three variables per + qubit. Taken from `[1] `__. + +Rather than seeking to independently distinguish each :math:`m_i`, magic +state rounding randomly selects a measurement basis which will perfectly +distinguish a particular pair of orthogonal QRAC states +:math:`\{ F(m), F(\bar m)\}`, where :math:`\bar m` indicates that +every bit of :math:`m` has been flipped. + +Let :math:`\mathcal{M}` be the randomized rounding procedure which takes +as input a state :math:`\rho_\text{relax}` and samples a bitstring +:math:`m` by measuring in a randomly selected magic-basis. + +.. math:: \mathcal{M}^{\otimes n}(\rho_\text{relax}) \rightarrow F(m) + +First, notice that for the :math:`(1,1,1)`-QRAC, there is only one basis +to choose and the magic state rounding scheme is essentially equivalent +to the semi-deterministic rounding scheme. + +For the :math:`(2,1,p)` and :math:`(3,1,p)` QRACs, if we apply the magic +state rounding scheme to an :math:`n`-qubit QRAC state :math:`F(m)`, we +will have a :math:`2^{-n}` and :math:`4^{-n}` probability of picking the +correct basis on each qubit to perfectly extract the solution :math:`m`. +Put differently, if we are given an unknown state :math:`F(m)` the +probability that :math:`\mathcal{M}^{\otimes n}(F(m))\mapsto F(m)` +decreases exponentially with the number of qubits measured - it is far +more likely to be mapped to some other :math:`F(m^*)`. Similarly, when +we perform magic rounding on an arbitrary state +:math:`\rho_\text{relax}`, we have similarly low odds of randomly +choosing the optimal magic basis for all :math:`n`-qubits. Fortunately +magic state rounding does offer a lower bound on the approximation ratio +under certain conditions. + +Let :math:`F(m^*)` be the highest energy state in the image of F, and +let :math:`\rho^*` be the maximal eigenstate of H. + +.. math:: \forall \rho_\text{relax}\quad \text{s.t.}\quad \text{Tr}\left(F(m^*)\cdot H\right) \leq \text{Tr}\left(\rho_\text{relax}\cdot H\right)\leq \text{Tr}\left(\rho^*\cdot H\right) + +.. math:: \frac{\text{expected fval}}{\text{optimal fval}} = \frac{\mathbb{E}\left[\text{Tr}\left(H\cdot \mathcal{M}^{\otimes n}(\rho_\text{relax})\right)\right]}{\text{Tr}\left(H\cdot F(m^*)\right)} \geq \frac{5}{9} + +References +---------- + +[1] Bryce Fuller et al., “Approximate solutions of combinatorial problems via quantum +relaxations,” (2021), `arXiv:2111.03167 `__, + +[2] Stephen Wiesner, “Conjugate coding,” SIGACT News, vol. 15, issue 1, +pp. 78-88, 1983. +`link `__ + +[3] Masahito Hayashi et al., +“(4,1)-Quantum random access coding does not exist—one qubit is not enough to recover +one of four bits,” New Journal of Physics, vol. 8, number 8, pp. 129, 2006. +`link `__ diff --git a/docs/index.rst b/docs/index.rst index 17031f403..ed8167ad9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,7 @@ Next Steps Migration Guide Tutorials API Reference + Explanations Release Notes GitHub diff --git a/docs/tutorials/12_quantum_random_access_optimizer.ipynb b/docs/tutorials/12_quantum_random_access_optimizer.ipynb new file mode 100644 index 000000000..93daf59f2 --- /dev/null +++ b/docs/tutorials/12_quantum_random_access_optimizer.ipynb @@ -0,0 +1,892 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Random Access Optimization" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Quantum Random Access Optimization (QRAO) module is designed to enable users to leverage a new quantum method for combinatorial optimization problems [1]. This approach incorporates Quantum Random Access Codes (QRACs) as a tool to encode multiple classical binary variables into a single qubit, thereby saving quantum resources and enabling exploration of larger problem instances on a quantum computer. The encodings produce a local quantum Hamiltonian whose ground state can be approximated with standard algorithms such as VQE, and then rounded to yield approximation solutions of the original problem.\n", + "\n", + "QRAO through a series of 3 classes:\n", + "1. The encoding class (`QuantumRandomAccessEncoding`): This class encodes the original problem into a relaxed problem that requires fewer resources to solve.\n", + "2. The rounding schemes (`SemideterministicRounding` and `MagicRounding`): This scheme is used to round the solution obtained from the relaxed problem back to a solution of the original problem.\n", + "3. The optimizer class (`QuantumRandomAccessOptimizer`): This class performs the high-level optimization algorithm, utilizing the capabilities of the encoding class and the rounding scheme.\n", + "\n", + "\n", + "*References*\n", + "\n", + "[1] Bryce Fuller et al., *Approximate Solutions of Combinatorial Problems via Quantum Relaxations,* [arXiv:2111.03167](https://arxiv.org/abs/2111.03167)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_optimization.algorithms.qrao import (\n", + " QuantumRandomAccessEncoding,\n", + " SemideterministicRounding,\n", + " QuantumRandomAccessOptimizer,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up a combinatorial optimization problem\n", + "\n", + "In this tutorial, we will consider a random max-cut problem instance and use QRAO to try to find a maximum cut; in other words, a partition of the graph's vertices (nodes) into two sets that maximizes the number of edges between the sets.\n", + "\n", + "To begin, we utilize the `Maxcut` class from Qiskit Optimization's application module. It allows us to generate a `QuadraticProgram` representation of the given graph.\n", + "\n", + "Note that once our problem has been represented as a `QuadraticProgram`, it will need to be converted to the correct type, a [quadratic unconstrained binary optimization (QUBO)](https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization) problem, so that it is compatible with QRAO.\n", + "A `QuadraticProgram` generated by `Maxcut` is already a QUBO, but if you define your own problem be sure you convert it to a QUBO before proceeding. Here is [a tutorial](https://qiskit.org/documentation/optimization/tutorials/02_converters_for_quadratic_programs.html) on converting `QuadraticPrograms`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Problem name: Max-cut\n", + "\n", + "Maximize\n", + " -2*x_0*x_1 - 2*x_0*x_3 - 2*x_0*x_4 - 2*x_1*x_2 - 2*x_1*x_5 - 2*x_2*x_3\n", + " - 2*x_2*x_4 - 2*x_3*x_5 - 2*x_4*x_5 + 3*x_0 + 3*x_1 + 3*x_2 + 3*x_3 + 3*x_4\n", + " + 3*x_5\n", + "\n", + "Subject to\n", + " No constraints\n", + "\n", + " Binary variables (6)\n", + " x_0 x_1 x_2 x_3 x_4 x_5\n", + "\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAHzCAYAAACe1o1DAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACOOElEQVR4nOzddVhVWcMF8EWr2IGNBaIi1higYo2NhV0cY9RxdOwWLEzEnNFxHPsidncDooAdiEopYgehCApc7vn+mFc+Z0YsLuwb6/c8PM/7ei/nLBzFxd777G0gy7IMIiIiIqLvZCg6ABERERFpNxZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFBZKIiIiIsoUFkoiIiIiyhQWSiIiIiLKFGPRAYh0VWKyElExiUhRqmBqbIiyhcxhbsa/ckREpHv4rxuRGoU/T4D3hWj4hL5AdGwS5I9eMwBgWTAXmtpYoE89S1gXzSMqJhERkVoZyLIsf/ltRPQ5D2OTMHVvMPwjXsHI0ABpqoz/Wn143dGqMOY526F0wVzZmJSIiEj9WCiJMmnbpWjMOBACpUr+bJH8NyNDAxgbGmBWB1v0rGOZhQmJiIiyFgslUSas8AnHohNhmb7O+JYV8WtTazUkIiIiyn58ypvoO227FK2WMgkAi06EYfulaLVci4iIKLtxhJLoOzyMTULzpX5IVqo++bqsTEW8/2YkhvhA9f4tTIqURf5GLshZrmaG1zQzNsSpMY25ppKIiLQORyiJvsPUvcFQfma95KvDS/Hm0j6YV2mCAs2HwMDQEC92zsT7hyEZfo5SJWPq3uCsiEtERJSlWCiJvlH48wT4R7zK8AGc5CehSLpzFvkb90OBZgORp0ZrFO01D8Z5LRDvuyHD66apZPhHvELEi4Ssik5ERJQlWCiJvpH3hWgYGRpk+HpS6HnAwBB5arRO/zUDY1Pkrt4CyY/vQvnmZYafa2RogM1BXEtJRETahYWS6Bv5hL747PZAKc/vwaRgSRia/XMtpGnxiumvZyRNJcMn7IV6ghIREWUTFkqib/A2WYno2KTPviftbSyMchf4z68b5S6Y/vrnRMckITFZ+f0hiYiIshkLJdE3eBCTiC9tiyArUwAjk//8uoGx6f+//rnPBxAVk/idCYmIiLIfCyXRN0jJYJugjxkYmwJpqf/59Q9F8kOxzOx9iIiINAULJdE3MDX+8l8Zo9wFkfY27j+//mGq+8PUd2bvQ0REpCn4rxbRNyhbyBwZP9/9N1OL8kiNfQxV8j/XWqY8+ftUHdOi5T/7+Qb/uw8REZG2YKEk+gbmZsaw/MJJNrkqNQBkFRKuH0v/NVmZirfBJ2FawgbGeYt8/vNVSQi7HQweYkVERNqChZLoGzW1sfjsPpRmJWyQq1JDxPttQpzPeiRcP4bnW6dC+foFCjQZ8NlrG8gqvL4TgFq1aqFatWrw9PTEkydP1P0lEBERqRXP8ib6RuHPE9Bi2dnPvkdWpiD+7N9neae9fwtTi7LI79gXOcv/8MXrHx1RH/evB0KhUGDfvn1ITU1F8+bNIUkSOnXqBHNzTocTEZFmYaEk+g4u6y4g4F7MZzc4/1ZGhgaoX74QvH6ql/5r8fHx2LVrFxQKBfz9/ZE7d2507doVkiShcePGMDTkJAMREYnHQkn0HR7GJqH5Uj8kq3F7HzNjQ5wa0xilM1ijee/ePWzevBkKhQKRkZEoXbo0XFxc4OLigkqVKqktBxER0bdioST6TtsuRWPynmC1Xc+jsx161LH84vtkWUZg4N9T4tu3b0d8fDzq1q0LSZLQo0cPFC5cWG2ZiIiIvgYLJVEmrPAJx6ITYZm+zoSWNhje1OqbP+/9+/c4dOgQFAoFjh49CgMDAzg5OcHFxQVOTk4wMzPLdDYiIqIvYaEkyqStF6MxZddVwMAQMDT6+k9UpcHM1ATuHWy/amTyS168eIFt27ZBoVDgypUrKFCgAHr27AlJklCvXj0YGHxpB00iIqLvwxX9RJlkFBWEx38NRZXCxn///89sKfTx6+8e3MDYSklqKZMAYGFhgZEjR+Ly5cu4desWhgwZggMHDsDBwQE2NjaYM2cOoqKi1HIvIiKij3GEkigT3r9/j8qVK8POzg4HDhxA+PMEeF+Ihk/YC0THJOHjv1wGACwL5ULTihboa2+Jkf17ICIiAiEhITAxMcmSfGlpafD19YVCocDu3buRmJiIxo0bQ5IkdO3aFXnz5s2S+xIRkX5hoSTKBA8PD7i5ueHWrVuwsbH5x2uJyUpExSQiRamCqbEhyhYyh7mZcfrrN27cQM2aNfHHH39g6NChWZ717du32Lt3LxQKBU6fPg0zMzM4OztDkiQ0b94cxsbGX74IERHRJ7BQEn2nFy9ewMrKCgMGDMDy5cu/6xqSJOHEiROIiIhA7ty51ZwwY48ePYK3tzc2bdqEO3fuoFixYujTpw8kSUK1atWyLQcREekGFkqi7zR06FDs2LEDERERKFiw4HddIyoqCjY2Npg2bRrc3NzUnPDLZFnG1atXoVAosGXLFrx69QrVq1eHJEno3bs3ihUrlu2ZiIhI+7BQEn2H4OBg1KhRA4sXL8bo0aMzda2xY8di7dq1iIyMRJEiRdQT8Dukpqbi2LFjUCgUOHDgAJRKJVq1agVJktCxY0fkzJlTWDYiItJsLJRE30iWZbRq1QpRUVG4desWTE1NM3W9V69eoUKFChgwYACWLVumnpCZFBcXhx07dkChUCAgIAB58+ZFt27dIEkSGjZsyCMfiYjoH1goib7RkSNH4OTkhH379qFjx45quea8efMwc+ZMhIaGoly5cmq5prqEh4enH/kYFRWFsmXLph/5aG1tLToeERFpABZKom+QmpqKatWqoXjx4jh9+rTaNgtPTEyEtbU1mjVrhs2bN6vlmuqmUqlw/vx5KBQK7NixA2/evIGDgwMkSUL37t2/ex0pERFpPxZKom+wcuVKjBgxAlevXkWNGjXUeu2//voLQ4cOzZJrq9u7d+9w4MABKBQKHD9+HEZGRmjXrh0kSUKbNm0yvQyAiIi0Cwsl0VeKj4+HlZUVOnbsiHXr1qn9+kqlEra2tihfvjyOHj2q9utnlWfPnmHr1q1QKBS4fv06ChUqhF69ekGSJNSuXZtHPhIR6QEWSqKvNH78ePz5558IDw9H8eLFs+Qee/bsQZcuXXD69Gk0a9YsS+6RlW7evAkvLy94e3vj6dOnqFSpEiRJQt++fVG6dGnR8YiIKIuwUBJ9hYiICFSpUgXTp0/P0v0iZVmGg4MD0tLScPHiRa0d3UtLS8Pp06ehUCiwZ88evH//Hk2bNoUkSejcuTPy5MkjOiIREakRCyXRV+jSpQsuXbqE0NDQLN+P0c/PD02aNMGOHTvQrVu3LL1Xdnjz5g327NkDhUIBHx8f5MqVC507d4YkSWjWrBmMjIxERyQiokxioST6gg8Fb/PmzejTp0+23LNdu3YIDQ3F7du3YWJiki33zA4PHjxIP/IxLCwMJUqUQN++fSFJEmxtbUXHIyKi78RCSfQZKpUKderUgbGxMQIDA7NtQ+/g4GBUr14dK1euxC+//JIt98xOsizj0qVLUCgU2Lp1K2JjY1GrVi1IkoRevXrBwsJCdEQiIvoGLJREn7Fp0yb0798f586dQ4MGDbL13v3798exY8cQERGB3LlzZ+u9s1NKSgqOHDkChUKBQ4cOQaVSoU2bNpAkCe3bt0eOHDlERyQioi9goSTKQGJiIipWrIiGDRti+/bt2X7/6OhoVKxYEa6urpg2bVq231+EmJgYbN++HQqFAhcuXEC+fPnQo0cPuLi4oEGDBlr7kBIRka5joSTKwMyZM7FgwQLcvXsXZcuWFZJh/PjxWL16Ne7du4ciRYoIySBKaGgovLy84OXlhejoaJQvXz79yMcKFSqIjkdERB9hoST6hEePHqFixYoYOXIkFixYICxHTEwMKlSogH79+mH58uXCcoikUqlw9uxZKBQK7Ny5E2/fvkWDBg3Sj3zMnz+/6IhERHqPhZLoE/r164ejR48iIiICefPmFZplwYIFmD59Ou7evYvy5csLzSJaUlIS9u3bB4VCgZMnT8LExAQdOnSAJElo1aqVTj0RT0SkTVgoif7l8uXLqFOnDv7880/8/PPPouMgKSkJ1tbWaNKkCby9vUXH0RhPnjzBli1bsGnTJty6dQtFihRB7969IUkSatasyfWWRETZiIWS6COyLKNx48aIi4vDtWvXYGxsLDoSAGDt2rUYPHgwrl69ipo1a4qOo1FkWcaNGzfSj3x8/vw5bG1tIUkS+vTpg5IlS4qOSESk81goiT6ye/dudO3aFcePH0fLli1Fx0mnVCphZ2cHS0tLHD9+XHQcjaVUKnHy5EkoFArs27cPycnJaN68OSRJgrOzM8zNzUVHJCLSSSyURP+TnJyMKlWqoFKlSjh8+LDoOP+xb98+ODs74+TJk2jevLnoOBrv9evX2LVrFxQKBc6ePQtzc3N07doVkiShSZMm2bZJPRGRPmChJPqfRYsWYfLkyQgODkblypVFx/kPWZbRoEEDpKSk4OLFiyxE3+D+/fvYvHkzFAoFIiIiULp0afTt2xcuLi4a+d+aiEjbsFASAXj58iWsrKzg4uKCFStWiI6TIX9/fzRq1Ajbtm1Djx49RMfROrIsIygoCAqFAtu2bUN8fDzq1KkDSZLQs2dPFC5cWHREIiKtxEJJBGD48OHw9vZGRESExpeKDh064Pbt27h9+zZMTU1Fx9FaycnJOHToEBQKBY4cOQIAcHJygiRJcHJygpmZmeCERETag4WS9N7t27dRrVo1eHh4YNy4caLjfNGtW7dQvXp1/Pbbbxg+fLjoODrh5cuX2LZtGxQKBS5fvowCBQqgR48ekCQJ9vb23IKIiOgLWChJ77Vt2xZhYWEICQnRmlGpgQMH4vDhw4iIiECePHlEx9Ept2/fhpeXFzZv3oxHjx7BysoKkiTBxcVF2BGcRESajoWS9Nrx48fRunVr7N69G507dxYd56s9fPgQ1tbWmDJlCmbMmCE6jk5KS0uDr68vFAoFdu/ejcTERDRq1AiSJKFr167Ily+f6IhERBqDhZL0llKpRI0aNVC4cGH4+Pho3bTmxIkTsWrVKkRERKBo0aKi4+i0xMRE7N27FwqFAqdOnYKZmRk6deoESZLQokULjdkAn4hIFBZK0lt//vknhg0bhsuXL6NWrVqi43yz2NhYVKhQAX379sXvv/8uOo7eePToUfqRj7dv30bRokXRp08fSJKE6tWri45HRCQECyXppdevX8Pa2hpt27bFxo0bRcf5bgsXLoSrqyvu3r2LChUqiI6jV2RZxrVr16BQKLBlyxa8fPkS1apVgyRJ6N27N4oXLy46IhFRtmGhJL00adIkrFixAmFhYVp91vO7d+9gbW0NR0dHbN26VXQcvZWamorjx49DoVBg//79UCqVaNmyJSRJQseOHZErVy7REYmIshQLJemde/fuoXLlypg6dapOPNCyfv16/PTTT7h8+TJ++OEH0XH0XlxcHHbu3AmFQoHz588jT5486NatGyRJgqOjI084IiKdxEJJeqd79+4ICAhAaGgozM3NRcfJNKVSierVq6NEiRI4efKk6Dj0kYiIiPQjH+/fv48yZcrAxcUFLi4uqFixouh4RERqw0JJeuXcuXNwdHTEpk2bIEmS6Dhqc+DAAXTs2BEnTpxAixYtRMehf5FlGefPn4dCocCOHTvw+vVr2NvbQ5Ik9OjRAwULFhQdkYgoU1goSW+oVCrY29tDlmVcuHBBp6YeZVmGo6MjkpKScPnyZZ362nTNu3fvcPDgQSgUChw7dgyGhoZo164d+vXrhzZt2vA4TSLSSiyUpDc2b94MFxcXnD17Fo6OjqLjqN358+fRsGFDbNmyBb169RIdh77C8+fPsXXrVigUCly7dg2FChVCz549IUkS6tSpo3V7oxKR/mKhJL2QlJQEGxsb1KtXD7t27RIdJ8t06tQJN2/exN27dznSpWWCg4PTj3x8+vQpbGxsIEkS+vbtC0tLS9HxiIg+i4WS9MLs2bMxZ84c3L59W6f3a7x9+zbs7OywbNkyjBgxQnQc+g5paWk4ffo0vLy8sGfPHrx79w5NmjSBJEno0qULz24nIo3EQkk678mTJ7C2tsawYcPg6ekpOk6WGzRoEPbv34/IyEjkzZtXdBzKhISEBOzZswcKhQI+Pj7IkSMHOnfuDEmS8OOPP8LIyEh0RCIiACyUpAcGDhyIgwcPIjw8HPnz5xcdJ8s9evQI1tbWmDhxImbNmiU6DqlJdHQ0vL29sWnTJoSGhqJEiRLpRz5WrVpVdDwi0nMslKTTrl69itq1a2PFihUYNmyY6DjZZtKkSVi5ciUiIiJQrFgx0XFIjWRZxuXLl6FQKLB161bExMSgZs2akCQJvXr1QtGiRUVHJCI9xEJJOkuWZTRr1gwvXrzAjRs3YGxsLDpStomLi0P58uXRu3dvrFy5UnQcyiIpKSk4evQoFAoFDh48CJVKhdatW0OSJLRv3x45c+YUHZGI9AQLJemsffv2wdnZGUePHkXr1q1Fx8l2np6emDp1Km7fvg1ra2vRcSiLxcTEYMeOHVAoFAgKCkK+fPnQvXt3SJKEBg0acAsiIspSLJSkk1JSUmBra4sKFSrg2LFjouMI8e7dO1SsWBH169fH9u3bRcehbBQWFgYvLy94eXnhwYMHKFeuXPqRj1ZWVqLjEZEOYqEknbR06VKMHz8eN2/ehK2treg4wmzYsAEDBw7ExYsXUadOHdFxKJupVCr4+/tDoVBg586dSEhIQP369SFJErp3744CBQqIjkhEOoKFknROTEwMrKys0LNnT6xatUp0HKHS0tJQvXp1WFhY4PTp05z21GNJSUnYv38/FAoFTpw4AWNjY3To0AGSJKF169YwMTERHZGItBgLJemckSNHYtOmTQgPD4eFhYXoOMIdPHgQHTp0wLFjx9CqVSvRcUgDPH36FFu2bIFCocDNmzdRpEgR9OrVC5IkoVatWvzBg4i+GQsl6ZS7d++iatWqmDdvHiZOnCg6jkaQZRmNGjVCQkICrl69CkNDQ9GRSIPcuHEDCoUC3t7eeP78OapUqQJJktCnTx+UKlVKdDwi0hIslKRT2rdvj5CQENy+fRs5cuQQHUdjBAQEoEGDBti8eTP69OkjOg5pIKVSiZMnT8LLywt79+5FcnIyfvzxR0iSBGdnZ+TOnVt0RCLSYCyUpDNOnTqFFi1aYMeOHejWrZvoOBrH2dkZ169fx927d2FmZiY6Dmmw169fY/fu3VAoFPDz84O5uTm6dOkCSZLQpEkTHvlIRP/BQkk6IS0tDTVr1kS+fPlw9uxZrgH7hDt37qBq1apYsmQJRo0aJToOaYmoqChs3rwZCoUC4eHhKFWqFPr27QsXFxdUqVJFdDwi0hAslKQT1qxZgyFDhnB7nC8YPHgw9u7di8jISOTLl090HNIisizjwoULUCgU2LZtG+Li4lC7dm1IkoSePXuiSJEioiMSkUAslKT13rx5A2tra7Rs2RJeXl6i42i0x48fw8rKCuPHj8fs2bNFxyEtlZycjMOHD0OhUODw4cMAgLZt20KSJLRr145LKoj0EAslab2pU6di2bJlCA0NRenSpUXH0XhTpkzBb7/9hoiICBQvXlx0HNJyr169wrZt26BQKHDp0iXkz58fPXv2hCRJsLe35/ITIj3BQklaLSoqCpUqVcLEiRPh7u4uOo5WiI+PR/ny5dGjRw+93/id1OvOnTvpRz4+evQIVlZW6Uc+litXTnQ8IspCLJSk1Xr16gU/Pz+EhYVxW5NvsHjxYkyaNAm3b99GxYoVRcchHaNSqeDr6wuFQoFdu3YhMTERjo6OkCQJ3bp14/pdIh3EQklaKzAwEPXr18f69esxYMAA0XG0yvv371GxYkXUq1cPO3fuFB2HdFhiYiL27t0LhUKBU6dOwczMDB07doQkSWjZsiWMjY1FRyQiNWChJK0kyzIcHByQkpKCy5cv8/SX77Bp0yb0798fQUFBqFevnug4pAceP36MLVu2YNOmTQgJCUHRokXRu3dvSJKE6tWrc70lkRZjoSSttHXrVvTu3Rs+Pj5o0qSJ6DhaKS0tDTVq1EChQoXg4+PDf8wp28iyjOvXr6cf+fjy5UvY2dlBkiT07t0bJUqUEB2RiL4RCyVpnXfv3sHGxgY//PAD9u7dKzqOVjt8+DDatWuHI0eOoE2bNqLjkB5KTU3FiRMnoFAosH//fqSmpqJFixaQJAmdOnVCrly5REckoq/AQklaZ968eZg5cyZCQkJgbW0tOo5Wk2UZTZo0QVxcHK5du8Yj9Uio+Ph47Ny5EwqFAufOnUPu3LnRrVs3SJKERo0acWkLkQZjoSSt8uzZM1hbW2Pw4MFYsmSJ6Dg6ISgoCA4ODlAoFHBxcREdhwgAEBkZmX7k471792BpaZm+BZGNjY3oeET0LyyUpFUGDx6MPXv2ICIiAgUKFBAdR2d06dIFly9fRmhoKHLkyCE6DlE6WZYREBAAhUKB7du34/Xr16hXrx4kSUKPHj1QqFAh0RGJCCyUpEVu3LiBmjVrYvny5RgxYoToODolNDQUtra28PT0xJgxY0THIfqk9+/f4+DBg1AoFDh69CgMDQ3Rrl07uLi4wMnJCaampqIjEuktFkrSCrIso3nz5njy5Alu3rwJExMT0ZF0zs8//4xdu3bh3r173HiaNN6LFy+wdetWKBQKXL16FQULFkw/8rFu3brctYAom7FQklY4ePAgOnTogEOHDsHJyUl0HJ305MkTWFlZYcyYMZg7d67oOERf7datW/Dy8sLmzZvx5MkTVKxYEZIkoW/fvihTpozoeER6gYWSNF5KSgrs7OxgaWmJEydOcOQhC7m6umLp0qWIiIjgXoCkddLS0nDmzBkoFArs2bMHSUlJaNKkCSRJQpcuXZA3b17REYl0FgslabzffvsNY8aMwfXr12FnZyc6jk57/fo1ypcvj65du2L16tWi4xB9t4SEBOzZswdeXl44c+YMcuTIAWdnZ0iShObNm3OLLCI1Y6EkjRYbGwsrKyt07doVf/31l+g4emHp0qWYMGECbt26hUqVKomOQ5RpDx8+hLe3NzZt2oS7d++iePHi6NOnDyRJ4g+pRGrCQkkabcyYMVi7di0iIiJQtGhR0XH0QnJycvpJRLt37xYdh0htZFnGlStXoFAosGXLFsTExKBGjRqQJAm9evVCsWLFREck0loslKSxwsLCYGtrC3d3d0yZMkV0HL3i5eUFSZIQGBgIe3t70XGI1C4lJQXHjh2DQqHAwYMHkZaWhlatWkGSJHTo0AE5c+YUHZFIq7BQksbq1KkTrl+/jrt373Kz7WyWlpaGWrVqIV++fPDz8+ODUKTTYmNjsWPHDigUCgQGBiJv3rzo3r07JElCgwYNeOQj0VdgoSSN5OPjg2bNmmHbtm3o0aOH6Dh66ejRo2jbti23aiK9Eh4eDi8vL3h5eSEqKgrlypVLP/LRyspKdDwijcVCSRonLS0NP/zwA3LlyoXz589zdEwQWZbRrFkzvHr1CtevX+dTsaRXVCoVzp07B4VCgR07diAhIQEODg6QJAndu3dHwYIFRUck0igcxyeNs2nTJty4cQNLlixhmRTIwMAAHh4euHXrFjZv3iw6DlG2MjQ0RKNGjbB27Vo8f/4cW7duRf78+TF8+HAUL14cXbt2xYEDB5Camio6KpFG4AglaZSEhARUrFgRTZs2xZYtW0THIQDdunXDhQsXEBYWxrWspPeePXuGLVu2QKFQ4MaNGyhcuDB69eoFSZLwww8/8Idg0lsslKRRpk2bhkWLFiE0NBSWlpai4xD+ftq+SpUq8PDwwLhx40THIdIYN27cgJeXF7y9vfHs2TNUrlwZkiShT58+KF26tOh4RNmKhZI0RnR0NGxsbDB27FieJa1hfvnlF2zfvh337t1D/vz5Rcch0ihKpRKnTp2CQqHA3r17kZycjGbNmkGSJHTu3Bm5c+cWHZEoy7FQksbo27cvTp8+jbCwMOTJk0d0HPrI06dPYWVlhZEjR2L+/Pmi4xBprDdv3mD37t1QKBTw9fVFrly50KVLF0iShKZNm/LhNtJZLJSkES5evIh69eph7dq1+Omnn0THoU/4sBwhIiICJUuWFB2HSONFRUWlH/kYHh6OkiVLom/fvpAkCVWqVBEdj0itWChJOFmW0bBhQyQmJuLKlSv8CV5DvXnzBuXLl4ezszPWrFkjOg6R1pBlGRcvXoRCocDWrVsRFxeHH374AZIkoWfPnrCwsBAdkSjTWChJuB07dqBHjx44deoUfvzxR9Fx6DOWL1+OsWPH4tatW6hcubLoOERaJzk5GUeOHIFCocDhw4chyzLatGkDSZLQrl077qRAWouFkoR6//49KleuDDs7Oxw4cEB0HPqC5ORkVKpUCTVq1MDevXtFxyHSaq9evcL27duhUChw8eJF5M+fHz169ICLiwvq16/PLYhIq7BQklAeHh5wc3PDrVu3YGNjIzoOfQVvb2/07dsX58+fR/369UXHIdIJd+/eTT/y8eHDh6hQoQIkSULfvn1Rvnx50fGIvoiFkoR5/vw5rK2tMWDAACxfvlx0HPpKKpUKtWrVQp48eXD27FmOohCpkUqlgp+fHxQKBXbt2oW3b9+iYcOGkCQJ3bp147ZdpLFYKEmYoUOHYseOHYiIiOC5uFrm+PHjaN26NQ4ePIh27dqJjkOkkxITE7Fv3z4oFAqcOnUKJiYm6NixIyRJQsuWLWFiYiI6IlE6FkoSIjg4GDVq1MCSJUswatQo0XHoG8myjObNm+P58+e4ceMGn8wnymKPHz9OP/Lx1q1bsLCwQO/evSFJEmrUqMGZAhKOhZKynSzLaNWqFaKionDr1i2YmpqKjkTf4dKlS6hbty42bNiA/v37i45DpBdkWcaNGzegUCjg7e2NFy9eoGrVqulHPpYoUUJ0RNJTLJSU7Y4cOQInJyfs378fHTp0EB2HMqFHjx4ICAhAWFgYcubMKToOkV5JTU3FyZMnoVAosG/fPqSmpqJ58+aQJAmdOnWCubm56IikR1goKVulpqaiWrVqKF68OE6fPs1pGi0XHh6OKlWqYP78+Rg/frzoOER6Kz4+Hrt27YJCoYC/vz9y586Nrl27QpIkNG7cGIaGhqIjko5joaRstXLlSowYMQJXr15FjRo1RMchNRg+fDi2bt2KyMhIFChQQHQcIr137949bN68GQqFApGRkShdujRcXFzg4uKCSpUqiY5HOoqFkrJNXFwcrK2t0bFjR6xbt050HFKTZ8+ewcrKCr/++isWLFggOg4R/Y8sywgMDIRCocD27dsRHx+PunXrQpIk9OjRA4ULFxYdkXQICyVlm/Hjx+PPP/9EeHg4ihcvLjoOqdGMGTOwcOFChIeHo1SpUqLjENG/vH//HocOHYJCocDRo0dhYGAAJycnSJKEtm3bwszMTHRE0nIslJQtIiIiUKVKFcyYMQOurq6i45CavXnzBlZWVujQoQPWrl0rOg4RfcaLFy+wbds2KBQKXLlyBQULFkTPnj3h4uKCevXqcW07fRcWSsoWnTt3xuXLlxEaGsqngXXU77//jtGjRyM4OBhVqlQRHYeIvkJISAi8vLywefNmPH78GNbW1ulHPpYtW1Z0PNIiLJSU5fz8/NCkSRN4e3ujd+/eouNQFklJSUGlSpVQrVo17Nu3T3QcIvoGaWlp8PHxgUKhwO7du5GUlITGjRtDkiR07doVefPmFR2RNBwLJWUplUqFOnXqwNjYGIGBgdy6Qsdt3boVvXv3xrlz59CgQQPRcYjoO7x9+xZ79uyBQqHAmTNnYGZmBmdnZ0iShObNm8PY2Fh0RNJALJSUpTZt2oT+/fvj/PnzqF+/vug4lMVUKhVq166NXLlywd/fn2uxiLTco0eP4O3tjU2bNuHOnTsoVqwY+vTpA0mSUK1aNdHxSIOwUFKWSUxMRMWKFdGwYUNs375ddBzKJidPnkTLli15EhKRDpFlGVevXoVCocCWLVvw6tUrVK9eHZIkoXfv3ihWrJjoiCQYCyVlmZkzZ2LBggW4e/cuF3frmRYtWuDJkye4ceMGp8eIdExqaiqOHTsGhUKBAwcOQKlUolWrVpAkCR07duSDl3qKhZKyxKNHj1CxYkWMGjUK8+fPFx2HstmVK1dQu3ZtrFu3DgMHDhQdh4iySFxcHHbs2AGFQoGAgADkzZsX3bp1gyRJaNiwIdfN6xEWSsoS/fr1w7FjxxAeHs6nA/VUr1694O/vj/DwcI5YEOmB8PDw9CMfo6KiULZs2fQjH62trUXHoyzGQklqd/nyZdSpUwerV6/GkCFDRMchQSIjI1GpUiXMnTsXEydOFB2HiLKJSqXC+fPnoVAosGPHDrx58wYODg6QJAndu3dHwYIFRUekLMBCSWolyzIaNWqE+Ph4XLt2jevn9NyIESOwefNmREZG8h8RIj307t07HDhwAAqFAsePH4eRkRHatWsHSZLQpk0bmJqaio5IasJCSWq1e/dudO3aFSdOnECLFi1ExyHBXrx4gQoVKuCXX37BwoULRcchIoGePXuGrVu3QqFQ4Pr16yhUqBB69eoFSZJQu3ZtbjOm5VgoSW2Sk5NRpUoVVKpUCYcPHxYdhzTErFmzMH/+fISHh6N06dKi4xCRBrh582b6kY/Pnj1DpUqV0o985PcJ7cRCSWqzaNEiTJ48GcHBwahcubLoOKQhEhISYGVlBScnJ6xfv150HCLSIEqlEqdPn4ZCocDevXvx/v17NG3aFJIkoXPnzsiTJ4/oiPSVWChJLV6+fAkrKytIkoTff/9ddBzSMCtXrsTIkSNx48YNVK1aVXQcItJAb968wZ49e7Bp0yb4+voiV65c6Ny5MyRJQrNmzWBkZCQ6In0GCyWpxfDhw+Ht7Y2IiAgULlxYdBzSMCkpKahSpQqqVKmCAwcOiI5DRBruwYMH6Uc+hoWFoUSJEujbty8kSYKtra3oePQJLJSUabdv30a1atWwcOFCjB07VnQc0lDbt29Hz549cfbsWTg6OoqOQ0RaQJZlXLp0CQqFAlu3bkVsbCxq1aoFSZLQq1cvWFhYiI5I/8NCSZnWpk0bhIeHIyQkBGZmZqLjkIZSqVSoW7cuTE1Ncf78eT7RSUTfJCUlBUeOHIFCocChQ4egUqnQpk0bSJKE9u3bI0eOHNmaJzFZiaiYRKQoVTA1NkTZQuYwN9PfrfJYKClTjh07hjZt2mDPnj1wdnYWHYc03OnTp9G8eXPs3bsXnTp1Eh2HiLRUTEwMtm/fDoVCgQsXLiBfvnzo0aMHJElC/fr1s+wH1vDnCfC+EA2f0BeIjk3CxwXKAIBlwVxoamOBPvUsYV1Uvx4oYqGk76ZUKlG9enUUKVIEPj4+HHGir9KqVStER0cjODiYG98TUaaFhobCy8sLXl5eiI6ORvny5dO3IKpQoYJa7vEwNglT9wbDP+IVjAwNkKbKuDp9eN3RqjDmOduhdMFcasmg6Vgo6bv9+eefGDZsGC5fvoxatWqJjkNa4tq1a6hVqxbWrFmDQYMGiY5DRDpCpVLh7NmzUCgU2LlzJ96+fYsGDRqkH/mYP3/+77rutkvRmHEgBEqV/Nki+W9GhgYwNjTArA626FnH8rvurU1YKOm7vH79GtbW1mjbti02btwoOg5pmT59+sDX1xfh4eHIlUs/fnonouyTlJSEffv2QaFQ4OTJkzAxMUGHDh0gSRJatWoFExOTr7rOCp9wLDoRluk841tWxK9NrTN9HU3GQknfZdKkSVixYgXCw8NRokQJ0XFIy9y7dw+VKlWCu7s7Jk+eLDoOEemwJ0+eYMuWLdi0aRNu3bqFIkWKoHfv3pAkCTVr1sxwuda2S9GYvCdYbTk8Otuhhw6PVLJQ0je7d+8eKleuDFdXV0yfPl10HNJSo0aNwqZNmxAZGYlChQqJjkNEOk6WZdy4cQNeXl7w9vbG8+fPYWtrC0mS0KdPH5QsWTL9vQ9jk9B8qR+Slar/XCfl5QO8PrcFKc8ikJYYDwMTM5gUKo289Tojl3W9DO9vZmyIU2Ma6+yaShZK+mbdunVDYGAgQkNDYW5uLjoOaamXL1+iQoUKGDJkCBYtWiQ6DhHpEaVSiZMnT0KhUGDfvn1ITk5G8+bNIUkSnJ2dMXTbLQTci/nkmsl3kZfw5vJBmJWsBKPcBSGnJiMpNADJj0JQsPWvyFOj9SfvaWRogPrlC8Hrp4xLpzZjoaRvcu7cOTg6OkKhUMDFxUV0HNJys2fPxpw5cxAWFoYyZcqIjkNEeuj169fYtWsXFAoFzp49i7ylKqJA3yXfdA1ZlYanG0dDVqai5JA/P/veU2MawcpC97YUYqGkr6ZSqVCv3t8/WV24cAGGhoaCE5G2e/v2LaysrNC6dWs+3EVEwt2/fx/D1/vidkohwPDbzg5/sXMWkp+Fo/SIzRm+x8jQAC71ymBmB907PpKNgL7ali1bcPnyZSxZsoRlktQid+7cmD59OhQKBYKD1bf4nYjoe5QrVw6Ject+VZlUpbxHWtJrpMY9xZuL+/Du3hXkKFP9s5+TppLhE/ZCTWk1C0co6askJSXBxsYG9erVw65du0THIR2SmpqKKlWqwMbGBocOHRIdh4j02NtkJexmHsfXFKOYYyvw9vqxv/+PgSFyVXRAwTYjYJQj92c/zwDArZmtdO6YRg4z0VdZvHgxXrx4gYULF4qOQjrGxMQEc+fOxeHDh+Hn5yc6DhHpsQcxiV9VJgEgb52OsOg5B4WcxiBn+R8gyyogLfWLnycDiIpJzFROTcQRSvqiJ0+ewNraGsOHD2ehpCzxYX2ukZERAgMDeYwnEQlxLToOzqsCvutzn2+bBlXyWxSTlnzxe9jeX+qjpmWB77qPpuIIJX2Rq6srcuXKBVdXV9FRSEcZGhrCw8MDFy5cwN69e0XHISI9ZWr8/bUoV6UGSHkaDmXs4yy9j6bSva+I1Orq1avYtGkT3N3dkS9fPtFxSIc1a9YMrVq1wpQpU6BUKkXHISI9VLaQOb53fkROTQYAqJI/P51t8L/76BoWSsqQLMsYO3YsKleujMGDB4uOQ3pgwYIFCAsLw/r160VHISI9ZG5mDMsvnGSTlhj/n1+T05RIvHUGBsZmMCn8+eMVLQvl0rkHcgBA974iUpv9+/fDz88PR48ehbEx/6hQ1qtRowb69OmDmTNnok+fPjyJiYiyXVMbC3hdePDJU3KAv5/ullOSYFa6KozyFELa2zgk3vaFMuYRCjT7CYamOTO8tpGhAZpWtMiq6ELxoRz6pJSUFNja2qJChQo4duyY6DikR+7fvw8bGxvMnDkTU6dOFR2HiPRM+PMEtFh2NsPXE2/74e3Nk0h5GQXVuwQYmuaEaTEr5Pmh/WfP8v6AJ+WQXlm6dCkmTJiAGzduwNZW93b0J802evRobNiwAZGRkShcuLDoOESkZ1zWXcjwLO/vpetneXMNJf1HTEwM3N3dMWTIEJZJEsLV1RWyLGPevHmioxCRHprnbAcjAwBqHHMzNjTAPGc7tV1P07BQ0n/MnDkTKpUKs2bNEh2F9FSRIkUwceJErFy5ElFRUaLjEJGeuX/rMl6fWQOocU9c9w62KP2FB360GQsl/cPdu3exatUquLm5oUiRIqLjkB4bM2YMChQogOnTp4uOQkR6ZNu2bWjRogVsc7zGcMfPP7H9tSa0tEGPOuq5lqbiGkr6h3bt2uH27du4c+cOzMzMRMchPffnn39i2LBhuHbtGqpXry46DhHpMFmWsXDhQkyePBkuLi5Yu3YtTE1Nse1SNGYcCIFSJX/TmkojQwMYGxrAvYOtzpdJgIWSPnLy5Em0bNkSO3fuRNeuXUXHIUJqaipsbW1hZWWFI0eOiI5DRDpKqVRixIgR+PPPPzFt2jTMmjXrH8cnPoxNwtS9wfCPeAUjAyDtM83JyNAAaSoZjlaFMc/ZTqenuT/GQkkAgLS0NNSsWRP58uXD2bNneZYyaYxdu3ahW7duOHPmDJo2bSo6DhHpmLdv36Jnz544duwY/vrrLwwcODDD94Y/T4DbpuM4FxkH04Il8HGBMsDfm5Y3rWiBvvaWOrk10OewUBIAYM2aNRgyZAguXryIOnXqiI5DlE6WZdjb20OWZVy4cIE/7BCR2jx79gxOTk4ICwvDrl270KpVqy9+Tv/+/XH9+nWcv3AZUTGJSFGqYGpsiLKFzHXyBJyvxUJJePPmDaytrdGqVSsoFArRcYj+w9fXF02bNuVyDCJSm9u3b6Nt27ZITU3F4cOHUaNGja/6vLJly8LZ2RlLly7N2oBahk95E+bPn4+EhATu+Ucaq0mTJmjTpg2mTp2K1NRU0XGISMv5+fmhQYMGyJs3L4KCgr66TN6/fx8PHjxAkyZNsjSfNmKh1HNRUVHpp+KUKlVKdByiDM2fPx8RERFYt26d6ChEpMW8vb3RokUL1K5dG/7+/ihduvRXf66vry8MDAzQqFGjLEyonTjlred69uyJs2fPIiwsDLlz5xYdh+izJEnCiRMnEBERwT+vRPRNZFnG/Pnz4erqiv79++Ovv/6CiYnJN11DkiTcunULV69ezaKU2osjlHosMDAQ27dvx7x58/iPM2kFd3d3xMXFYdmyZaKjEJEWUSqV+Pnnn+Hq6opZs2Zh/fr131wmZVlOX89N/8URSj2lUqlQv359pKSk4PLlyzA05M8WpB3Gjh2LtWvXIjIykqc5EdEXJSQkoHv37jh16hTWrl2Lfv36fdd1IiMjYWVlhQMHDqB9+/ZqTqn92CL01Pbt23HhwgUsXbqUZZK0ytSpU2FgYIC5c+eKjkJEGu7Jkydo1KgRAgICcPTo0e8uk8Df6ycNDQ3h6OioxoS6gyOUeujdu3ewsbFB7dq1sWfPHtFxiL7ZvHnzMHPmTISGhqJcuXKi4xCRBrp16xbatm0LWZZx5MgR2NnZZep6ffv2xd27d3H58mU1JdQtHJrSQ0uWLMGzZ8+wcOFC0VGIvsuoUaNQuHBhTJs2TXQUItJAZ86cQYMGDVCwYEEEBQVlukxy/eSXsVDqmWfPnmH+/PkYMWIErKysRMch+i7m5uaYOXMmvL29ce3aNdFxiEiDeHl5oXXr1nBwcMDZs2dRsmTJTF8zIiICjx8/5v6Tn8FCqWfc3NyQI0cOuLm5iY5ClCkDBw5ExYoVMWXKFNFRiEgDyLKM2bNnQ5IkSJKEgwcPIm/evGq5NtdPfhkLpR65ceMG1q9fj5kzZ6JAgQKi4xBlirGxMebPn4/jx4/j9OnTouMQkUCpqakYNGgQpk+fjjlz5mDNmjXfvC3Q5/j4+OCHH35QW0HVRXwoR0/IsozmzZvjyZMnuHnzplr/ohGJIssyHBwcoFQqcfHiRe5YQKSH3rx5g65du8LX1xfr169H37591Xp9WZZRsmRJuLi4wMPDQ63X1iX87qsnDh06hDNnzmDRokUsk6QzDAwM4OHhgStXrmDXrl2i4xBRNnv06BEcHR1x8eJFHD9+XO1lEgDCwsLw9OlTrp/8Ao5Q6oGUlBTY2dmhTJkyOH78OAwMDERHIlKrdu3a4e7du7hz5w5/YCLSEzdv3kTbtm1hZGSEI0eOwNbWNkvus3r1agwfPhxxcXHIkydPltxDF3CEUg+sWrUKERERWLx4Mcsk6aT58+fj3r17WLNmzX9eS0xWIuTJa1yLjkPIk9dITFYKSEhE6nTy5Ek0bNgQFhYWCAoKyrIyCfy9frJ27dosk1/AEUodFxsbCysrK3Tr1g2rV68WHYcoy/Tv3x9Hjx5FZGQknibK8L4QDZ/QF4iOTcLH3+QMAFgWzIWmNhboU88S1kX5jwSRNtmwYQOGDBmCFi1aYMeOHcidO3eW3UuWZRQvXhwDBgzA/Pnzs+w+uoCFUseNHj0a69evR3h4OIoWLSo6DlGWiY6ORuXaDVF98AI8UeWDkaEB0lQZf3v78LqjVWHMc7ZD6YK5sjEtEX0rWZYxa9YszJo1C0OGDMHKlSthbGycpfe8c+cOqlSpgmPHjqFVq1ZZei9txylvHRYWFoaVK1di6tSpLJOk8wKeA0UHrsBj5d+jFZ8rkx+/HnAvBs2X+mHbpegsz0hE3yclJQUDBgzArFmzMH/+fPz5559ZXiaBv/efNDY2RoMGDbL8XtqOI5Q6rGPHjrhx4wbu3r2LHDlyiI5DlGVW+IRj0YmwTF9nfMuK+LWptRoSEZG6vH79Gl26dIG/vz82bNiA3r17Z9u9u3fvjsePH+P8+fPZdk9tlfX1noQ4c+YMDhw4gG3btrFMkk7bdilaLWUSABadCEOR3GboUcdSLdcjosx5+PAh2rZti0ePHuHEiRNo3Lhxtt37w/ndgwcPzrZ7ajOOUOqgtLQ0/PDDD8iVKxfOnz/PJ7tJZz2MTULzpX5IVqq++N7XAdsRf9YLJoUtUWLQHxm+z8zYEKfGNOaaSiLBrl+/DicnJ5iamuLIkSOoXLlytt4/JCQEVatWxcmTJ9G8efNsvbc24hpKHbRp0ybcuHEDS5cuZZkknTZ1bzCUX1grCQDKN6/wOnAHDEy+PFqvVMmYujdYHfGI6DsdO3YMjo6OKF68OAIDA7O9TAJ/r580MTFB/fr1s/3e2oiFUsckJCTA1dUVvXv3Rr169UTHIcoy4c8T4B/x6osP3wBAnM86mJWwgWkxqy++N00lwz/iFSJeJKgjJhF9o7Vr16Jdu3Zo0qQJ/Pz8UKxYMSE5fHx8UK9ePeTKxdmKr8FCqWM8PDwQHx/P/bJI53lfiIaR4ZdH4N9H30LS3fMo8OOQr762kaEBNgfxqW+i7CTLMtzc3DB48GAMGTIEe/fuhbm5uZAsKpUKvr6+PG7xG7BQ6pDo6GgsXrwY48aNg6UlHyog3eYT+uKLo5OyKg2xJ/9E7uotYWpR9quvnaaS4RP2IpMJiehrpaSkQJIkzJ07FwsXLsyWPSY/JyQkBDExMWjatKmwDNqGT3nrkClTpiB//vyYNGmS6ChEWeptshLRsUlfft+1o1C+eYmiveZ+8z2iY5KQmKyEuRm/TRJlpfj4eHTu3Bnnz5/Htm3b0KNHD9GR4OPjA1NTUzg4OIiOojX4nVJHXLhwAVu2bMHatWt53ijpvAcxifjSysm0d28Q7++N/PV7wChXvm++hwwgKiYRtiW+/XOJ6Os8ePAAbdu2xdOnT3Hq1Ck4OjqKjgTg7wdy7O3tkTNnTtFRtAanvHWALMsYO3Ysqlevjv79+4uOQ5TlUr5im6D4s14wzJkbeWq3z9L7ENH3uXr1Kuzt7fHu3TsEBgZqTJlUqVTw8/Pj+slvxBFKHbBz504EBATg9OnTMDIyEh2HKMuZGn/+Z+HU2Md4e/04Cvw4GGkJsem/LqelQlalQRn/HAZmuWCU8/Oj+V+6DxF9nyNHjqB79+6wtbXFgQMHNOp44ODgYMTGxnL95DdiodRy79+/x6RJk9ChQwc0a9ZMdByibFG2kDkMgAynvdMSYgBZhbhTqxF3avV/Xn/850/IU7sDCjbP+Mlvg//dh4jUa/Xq1Rg2bBjat2+PLVu2aNy2PD4+PjAzM4O9vb3oKFqFhVLLLVu2DI8ePcLx48dFRyHKNuZmxrAsmAsPMngwx6RIGRTp7PqfX48/6wVVyjsUbD4ExvmLf/YeloVy8YEcIjVSqVRwdXXFggULMGLECCxdulQjZ9V8fX3h4ODAY4u/Eb9barHnz59j3rx5GD58OCpWrCg6DlG2ampjAa8LDz65dZBRrnzIVfG/T2e+ubQfAD752j8+39AATStaqCcoESE5ORn9+/fH9u3bsWTJEowePVojT3JLS0uDn58fRo8eLTqK1uECIS02ffp0GBsbY/r06aKjEGW7PvUsv+qUnO+RppLR1557uRKpQ2xsLFq2bIm9e/dix44dGDNmjEaWSQC4efMm4uPjuX7yO3CEUksFBwdj7dq1WLJkCQoWLCg6DlG2sy6aB45WhRFwL+ari2WxPgu++B5DA6BBhcKwsuD2W0SZdf/+fbRt2xYvX77EmTNnNP5cbB8fH+TIkYNHF38HjlBqIVmWMW7cOFhZWWHYsGGi4xAJM8/ZDsZfcfziV5NlpKUko/Tzc1Aqleq7LpEeunz5Muzt7ZGamorAwECNL5PA3+sn69evDzMzM9FRtA4LpRY6evQoTp48CU9PT5iYmIiOQyRM6YK5MKuDrfouaGCAukYP4DFtIurXr4+QkBD1XZtIjxw8eBCNGzdG+fLlERgYCGtra9GRvigtLQ1nz57l/pPfiYVSy6SmpmLcuHFo1qwZ2rf//g2biXRFzzqWGN9SPQ+lTWhpg50eYxAQEIC3b9+iVq1amDdvHkcrib7BH3/8gU6dOqFVq1Y4c+YMihQpIjrSV7l+/Tpev37N9ZPfiYVSy6xevRqhoaFYvHixxi5qJspuvza1xoLOdjAzNoTRN06BGxkawMzYEB6d7TC8qRUAoF69erh69SrGjh2LadOmwd7eHsHBwVkRnUhnqFQqTJgwAcOHD8fIkSOxc+dOrTq60MfHBzlz5kSdOnVER9FKBrIsZ81jkqR2cXFxsLa2RqdOnbB27VrRcYg0zsPYJEzdGwz/iFcwMjT47MM6H153tCqMec52KF3w05srX7x4EQMGDEB4eDimT5+OSZMmcakJ0b+8f/8ekiRh165dWLp0KUaNGiU60jdr164dkpOTcfLkSdFRtBILpRYZN24cVq9ejfDwcBQv/vlNmYn0WfjzBHhfiIZP2AtEvUr8x2i+Af7etLxpRQv0tbf8qqe5k5OT4e7uDg8PD1SrVg0bN25EtWrVsvArINIeMTEx6NixI65cuYItW7bA2dlZdKRvplQqUbBgQUyaNAmurv89FIG+jIVSS0RERKBKlSqYMWMG/7ATfaXk5GTkzJMf81esRcvWbWFqbIiyhcy/+wScy5cvY8CAAQgNDYWbmxumTJnC0UrSa5GRkWjbti1iY2Nx8OBBrT2u8NKlS6hbty7Onz+vFU+jayKuodQSEydORLFixTB27FjRUYi0RkxMDOTU96haMj9qWhaAbYl8mTpOsXbt2rh8+TImTZoEd3d31K1bF9evX1dfYCItcuHCBTg4OECWZQQFBWltmQT+Xj+ZK1cu1K5dW3QUrcVCqQX8/Pywd+9eLFiwQKsWOBOJFhsbCwAoVKiQ2q5pZmaG2bNn48KFC0hLS0OdOnUwc+ZMpKSkqO0eRJpu//79aNq0KaytrREQEIAKFSqIjpQpvr6+aNiwIUxNTUVH0VoslBpOpVJh7NixqFevHnr16iU6DpFWiYmJAYAsOU3qhx9+wOXLlzF16lTMnTsXderUwbVr19R+HyJN8/vvv8PZ2RlOTk44deoUChcuLDpSpqSmpsLf35/7T2YSC6WGUygUuHr1KpYsWcJtgoi+0YdCqc4Ryo+Zmppi1qxZuHjxIgwMDFC3bl1Mnz6do5Wkk1QqFcaNG4eRI0di3Lhx2L59u07Mml29ehVv377l/pOZxEKpwRITEzF16lT06NGDi4SJvkNsbCwMDAyQP3/+LL1PzZo1cfHiRbi5uWH+/PmoXbs2rly5kqX3JMpO7969Q/fu3bFs2TL8/vvv8PT0hKGhblQIHx8fmJub44cffhAdRavpxp8GHbVw4ULExsZiwYIFoqMQaaWYmBjkz58fRkZGWX4vU1NTzJgxA5cvX4aRkRHq1asHNzc3JCcnZ/m9ibLSy5cv8eOPP+LIkSPYu3cvfv31V9GR1MrX1xeOjo7csSGTWCg11KNHj+Dp6YkxY8agbNmyouMQaaWYmJgsm+7OSPXq1XHx4kXMmDEDCxcuTF9rSaSNIiIiUL9+fURGRsLX1xcdOnQQHUmtUlNTce7cOa6fVAMWSg01depU5MmTB1OmTBEdhUhriSiUAGBiYoJp06bh8uXLMDMzg729PaZOncrRStIqgYGBsLe3h5GREYKCglC3bl3RkdTu8uXLSExM5PpJNWCh1ECXL1+Gl5cXZs+ejbx584qOQ6S1YmNjhRTKD6pVq4agoCDMmjULixYtQq1atXDx4kVheYi+1u7du9GsWTNUqVIFAQEBKFeunOhIWcLHxwd58uRBrVq1REfReiyUGkaWZYwZMwZ2dnb46aefRMch0moxMTFZsmXQtzAxMYGrqyuuXr2KnDlzwsHBAZMnT8b79++F5iLKyLJly9CtWzd06tQJJ06cEP53KCt9WD9pbPz9Bx7Q31goNczu3btx7tw5LF68OFseJCDSZaKmvD+latWqCAoKwpw5c7B06VLUrFkTQUFBomMRpUtLS8OoUaMwZswYTJw4Ed7e3siRI4foWFkmJSUF58+f5/pJNWGh1CDJycmYOHEinJyc0KJFC9FxiLSeJhVKADA2NsaUKVNw9epV5MmTBw0aNMDEiRPx7t070dFIzyUlJaFr165YsWIFVq1ahQULFujMtkAZuXTpEpKSkrh+Uk10+0+Llvntt98QHR0NT09P0VGItJ4sy8LXUGbE1tYWAQEBmDdvHpYvX46aNWsiMDBQdCzSUy9evECzZs1w4sQJ7N+/H0OHDhUdKVv4+Pggb968qFGjhugoOoGFUkO8fPkSc+bMwS+//ILKlSuLjkOk9d6+fYvU1FSNXf9lbGyMSZMm4dq1a8ifPz8aNGiA8ePHc7SSslVYWBgcHBwQFRUFPz8/tGvXTnSkbOPr64tGjRpx/aSasFBqiBkzZsDAwAAzZswQHYVIJ2T1sYvqUqVKFZw/fx4eHh5YsWIFatSogfPnz4uORXrg/PnzcHBwgJmZGYKCglC7dm3RkbJNcnIy10+qGQulBggJCcHq1asxffp0FC5cWHQcIp2gLYUSAIyMjDBhwgRcv34dBQsWhKOjI8aOHYukpCTR0UhH7dy5Ez/++CPs7Oxw/vx5vTtA4+LFi3j//j3XT6oRC6UGGD9+PMqXL69zx1kRiRQbGwtAOwrlB5UqVcK5c+fg6emJVatWoXr16vD39xcdi3SILMtYtGgRunfvji5duuD48eMoUKCA6FjZzsfHB/nz50f16tVFR9EZLJSCHTt2DMeOHcPChQthamoqOg6RzvgwQqmpaygzYmRkhHHjxuH69euwsLBA48aNMXr0aCQmJoqORlouLS0NI0aMwIQJE+Dq6orNmzfDzMxMdCwhPqyf5PZ86sNCKZBSqcS4cePQuHFjdOrUSXQcIp0SExMDExMT5M6dW3SU72JjY4OzZ89i8eLFWL16NapXr46zZ8+KjkVaKjExEc7Ozvjzzz/x119/Yc6cOTAwMBAdS4j3798jICCA091qxkIp0Jo1a3Dnzh0sWbJEb/9iE2WVD3tQavPfLSMjI4wZMwY3b95E8eLF0bhxY4wYMQJv374VHY20yPPnz9GkSRP4+Pjg4MGDGDx4sOhIQl24cAHJycl8IEfNWCgFef36NaZPn45+/frxDFGiLBAbG6t1090Zsba2hp+fH5YtW4Z169ahWrVq8PX1FR2LtMDdu3dhb2+Px48f4+zZs2jTpo3oSML5+PigQIECqFatmugoOoWFUpC5c+ciKSkJc+fOFR2FSCdp2ik5mWVoaIhRo0bh5s2bKFWqFJo2bYrhw4dztJIydPbsWdSvXx/m5uYICgpCzZo1RUfSCD4+PmjcuLHOnwSU3fi7KcC9e/ewfPlyTJo0CSVKlBAdh0gn6Vqh/MDKygq+vr747bffsHHjRtjZ2eHMmTOiY5GG2bZtG1q0aIEaNWrg3LlzsLS0FB1JI7x79w5BQUFcP5kFWCgFmDRpEooUKYLx48eLjkKks3S1UAJ/j1aOGDECN2/eRJkyZfDjjz9i2LBhSEhIEB2NBJNlGR4eHujVqxd69OiBY8eOIX/+/KJjaYzAwECkpKRw/WQWYKHMZv7+/ti1axfmz5+PXLlyiY5DpLN0aQ1lRipUqIAzZ85gxYoVUCgUsLOzw+nTp0XHIkGUSiWGDRuGyZMnY9q0adi0aRO3o/sXX19fFCpUCFWrVhUdReewUGYjlUqFsWPHonbt2ujTp4/oOEQ6TZdHKD9maGiI4cOH4+bNmyhfvjyaN2+OoUOH4s2bN6KjUTZ6+/YtOnbsiLVr12LdunVwd3fX6h0OsgrXT2Yd/o5mI29vb1y+fBlLlizhH2aiLJSWlob4+Hi9KJQflC9fHqdOncKqVavg7e0NOzs7nDx5UnQsygZPnz5F48aN4e/vj8OHD2PgwIGiI2mkpKQkXLhwgesnswhbTTZJSkrClClT0LVrVzg6OoqOQ6TT4uPjIcuyXhVK4O/RyqFDhyI4OBjW1tZo2bIlhgwZgtevX4uORlnk9u3bsLe3x/Pnz+Hv74+WLVuKjqSxAgICkJqayvWTWYSFMpssWrQIL1++hIeHh+goRDpPW49dVJeyZcvi5MmTWL16NbZu3YqqVavi+PHjomORmvn6+qJ+/frIly8fgoKCeC71F/j6+qJw4cKwtbUVHUUnsVBmgydPnsDDwwOjRo1C+fLlRcch0nkfCqW+jVB+zMDAAEOGDMGtW7dQuXJltG7dGoMGDeJopY7w9vZGy5YtUadOHfj7+6NUqVKiI2k8Hx8fNGnShGtLswgLZTZwdXVFrly54OrqKjoKkV5gofx/ZcqUwfHjx7FmzRrs2LEDtra2OHLkiOhY9J1kWca8efPQt29f9OnTB0eOHEG+fPlEx9J4iYmJuHjxItdPZiEWyix29epVbNq0Ce7u7vxLT5RNYmNjAejvlPe/GRgYYNCgQbh16xaqVq0KJycnDBgwAPHx8aKj0TdQKpX4+eef4erqilmzZmH9+vUwMTERHUsrnD9/HkqlkusnsxALZRaSZRljx45F5cqVMXjwYNFxiPRGTEwMzM3NYWZmJjqKRrG0tMTRo0exbt067NmzB7a2tjh8+LDoWPQVEhIS0L59e2zYsAEbN27E9OnTOXX7DXx9fWFhYYHKlSuLjqKzWCiz0L59++Dn54fFixfD2NhYdBwivaEve1B+DwMDAwwcOBAhISGoVq0a2rVrh379+iEuLk50NMrAkydP0KhRIwQEBODo0aPo16+f6Ehah+snsx4LZRZJSUnBhAkT0Lp1a7Ru3Vp0HCK9wkL5ZaVKlcKRI0ewYcMG7N+/H7a2tjh48KDoWPQvt27dgr29PV69eoVz586hefPmoiNpnbdv3+LSpUtcP5nFWCizyIoVKxAVFYVFixaJjkKkd2JjY1kov4KBgQH69++PkJAQ1KxZEx06dICLi0v6GlQS6/Tp02jQoAEKFiyIoKAg2NnZiY6klc6dO4e0tDSun8xiLJRZ4NWrV3B3d8eQIUO43xWRADExMXwg5xuULFkShw4dwqZNm3Do0CHY2tpi//79omPpNYVCgdatW8PBwQH+/v4oWbKk6Ehay9fXF8WKFYONjY3oKDqNhTILzJo1C7IsY9asWaKjEOklTnl/OwMDA0iShJCQENSuXRudOnVCnz590rdgouwhyzLc3d3Rr18/9O/fHwcPHkSePHlEx9JqXD+ZPVgo1ezOnTtYtWoV3NzcUKRIEdFxiPQSC+X3K1GiBA4cOAAvLy8cPXoUtra22Lt3r+hYeiE1NRU//fQTZsyYgTlz5uCvv/7itkCZ9ObNG1y5coXrJ7MBC6WaTZgwAZaWlhg5cqToKER6KzY2llPemWBgYIC+ffsiJCQE9erVQ+fOndGrVy+8evVKdDSd9ebNGzg5OWHz5s3w8vKCq6srR9TUgOsnsw8LpRqdPHkShw8fxsKFC7n/HZEgycnJSExM5AilGhQvXhz79u2Dt7c3Tpw4AVtbW+zevVt0LJ3z6NEjODo64uLFizh+/Dj69u0rOpLO8PX1RYkSJWBtbS06is5joVSTtLQ0jB07Fg0bNkSXLl1ExyHSWzx2Ub0MDAzQu3dvhISEoH79+ujatSt69OiBly9fio6mE27evAl7e3vEx8fj/PnznJpVM66fzD4slGqybt063Lp1C0uWLOEfXCKBWCizRrFixbBnzx5s3boVp0+fhq2tLXbu3Ck6llY7ceIEGjZsCAsLCwQFBXFXEDV7/fo1rl69ypKeTVgo1eDNmzeYNm0aXFxcUKdOHdFxiPQaz/HOOgYGBujZsydCQkLg6OiI7t27o1u3bnjx4oXoaFpnw4YNcHJyQsOGDXH27FkUL15cdCSd4+/vD5VKxfWT2YSFUg3mz5+PhIQEzJs3T3QUIr3HEcqsV7RoUezatQvbt2+Hr68vqlSpgu3bt0OWZdHRNJ4sy5gxYwYGDhyIn376CQcOHEDu3LlFx9JJvr6+KFWqFCpUqCA6il5gocykqKgoLF26FBMmTECpUqVExyHSezExMTAwMED+/PlFR9FpBgYG6N69O0JCQtCsWTP07NkTXbt2xfPnz0VH01gpKSno378/3N3dsWDBAqxatQrGxsaiY+ksrp/MXiyUmTR58mQULFgQEydOFB2FiPD3lHeBAgVgZGQkOopesLCwwI4dO7Bjxw74+/ujSpUq2Lp1K0cr/+X169do06YNtm3bhi1btmDSpEksOlkoPj4e165d4/rJbMRCmQkBAQHYvn075s2bB3Nzc9FxiAg8dlGUbt26ISQkBC1atEDv3r3RuXNnPHv2THQsjfDw4UM0bNgQV69excmTJ9GrVy/RkXTe2bNnIcsy109mIxbK76RSqTBmzBjUqlULkiSJjkNE/8NTcsQpUqQItm3bhl27diEgIABVqlSBt7e3Xo9WXr9+Hfb29nj79i0CAgLQqFEj0ZH0gq+vLywtLVGuXDnRUfQGC+V32rZtGy5evIglS5bA0JC/jUSagoVSvC5duiAkJAStW7dG37590alTJzx9+lR0rGx37NgxODo6onjx4ggMDETlypVFR9IbXD+Z/diEvsO7d+8wefJkODs7o3HjxqLjENFHYmNjWSg1QOHChbFlyxbs2bMHFy5cQJUqVeDl5aU3o5Vr165Fu3bt0KRJE/j5+aFYsWKiI+mN2NhY3Lhxg+snsxkL5XdYsmQJnj17hoULF4qOQkT/wjWUmsXZ2RkhISFwcnKCJEno0KEDnjx5IjpWlpFlGW5ubhg8eDCGDBmCvXv3co19NuP6STFYKL/Rs2fPMH/+fIwYMQJWVlai4xDRv3DKW/MUKlQImzdvxr59+3D58mXY2tpi06ZNOjdamZycDBcXF8ydOxcLFy7EypUruS2QAL6+vihbtizKli0rOopeYaH8Rm5ubsiRIwemTZsmOgoR/YssyyyUGqxjx44ICQlB+/bt0b9/f7Rr1w6PHz8WHUst4uLi0Lp16/QN3ydMmMD1e4L4+PhwulsAFspvcP36daxfvx4zZ87kpslEGujt27dQKpWc8tZgBQsWhEKhwIEDB3Dt2jXY2tpiw4YNWj1a+eDBAzRo0AA3b97EqVOn0L17d9GR9FZMTAxu3rzJ6W4BWCi/kizLGDduHGxsbPDzzz+LjkNEn8BjF7VH+/btERISgk6dOmHgwIFo27YtHj58KDrWN7ty5Qrs7e3x/v17BAQEoGHDhqIj6TU/Pz8AYKEUgIXyKx08eBBnzpzBokWLYGJiIjoOEX0CC6V2KVCgADZu3IhDhw7h5s2bqFq1KtatW6c1o5VHjhxB48aNYWlpiaCgINjY2IiOpPd8fX1Rvnx5WFpaio6id1gov0JKSgrGjx+PFi1aoG3btqLjEFEGWCi1k5OTE0JCQtClSxcMGjQIrVu3RnR0tOhYn7V69Wq0b98ezZs3h4+PDywsLERHInD9pEgslF9h1apViIyMxOLFi7nImkiDxcbGAgDXUGqh/PnzY/369Thy5AhCQkJQtWpVrFmzRuNGK1UqFaZMmYKhQ4di+PDh2L17N3LlyiU6FgF4+fIlbt26xeluQVgovyA2NhazZs3CoEGDYGdnJzoOEX1GTEwMTExMkDt3btFR6Du1adMGISEh6N69O4YMGYKWLVviwYMHomMB+HtboD59+sDDwwNLlizB8uXLYWRkJDoW/Q/XT4rFQvkF7u7uUCqVcHd3Fx2FiL7gw5ZBnEnQbvny5cPatWtx7Ngx3L17F1WrVsXq1auFjlbGxsaiZcuW2Lt3L3bs2IExY8bwz5mG8fHxgZWVFUqVKiU6il7S+0KZmKxEyJPXuBYdh5Anr5GYrEx/LTQ0FCtXrsTUqVNRtGhRgSmJ6GtwD0rd0qpVK4SEhKB3794YOnQomjdvjqioqGzPcf/+fdSvXx8hISE4c+YMunbtmu0Z6Mt8fX25flIgvdzCP/x5ArwvRMMn9AWiY5Pw8c+8BgAsC+ZCUxsLBHotRMmSJTF69GhBSYnoW8TGxnL9pI7JmzcvVq9eja5du2LQoEGoWrUqPD098fPPP8PQMOvHRC5duoR27dohT548CAwMhLW1dZbfk77d8+fPcfv2bbi6uoqOorf0aoTyYWwSXNZdQItlZ+F14QEe/KtMAoAM4EFsEhRBUQi16g7rIcvxMkklIi4RfSOOUOquFi1aIDg4GC4uLhg2bBiaN2+O+/fvZ+k9Dxw4gCZNmqB8+fIskxqO6yfF05tCue1SNJov9UPAvb+3FUlTfX4tzoeX7yWaoPlSP2y7pNlbWBARC6Wuy5s3L1atWoVTp07h3r17sLOzw8qVK6FSqf+H/pUrV8LZ2RmtW7fGmTNnUKRIEbXfg9THx8cHFStWRIkSJURH0Vt6UShX+IRj8p5gJCtVXyyS/5amkpGsVGHynmCs8AnPooREpA4slPrhxx9/RHBwMPr164dff/0VzZo1Q2RkpFqurVKpMGHCBPz6668YNWoUduzYgZw5c6rl2pR1uH5SPJ0vlNsuRWPRiTC1XGvRiTBs50glkcbiGkr9kSdPHqxcuRJnzpzBgwcPUK1aNfz++++ZGq18//49evbsicWLF2P58uVYsmQJtwXSAk+fPsXdu3c53S2YThfKh7FJmHEg5JOvqVLeId7fG8+3T8fDZT3xYEE7vL156ovXnH4gBA9jk9QdlYgyKS0tDfHx8Ryh1DNNmzZFcHAwBgwYgJEjR6JJkyaIiIj45uvExMSgefPmOHjwIHbv3o2RI0dmQVrKClw/qRl0ulBO3RsMZQZT3KqkN3h9fitSYx7CxKLcV19TqZIxdW+wuiISkZrExcVBlmUWSj2UO3durFixAj4+Pnj8+DGqVauGZcuWffVoZWRkJOrXr4/Q0FD4+PjA2dk5ixOTOvn4+KBSpUooVqyY6Ch6TWcLZfjzBPhHvMpwzaRR7oIo9asXSg3bgAJNB371ddNUMvwjXiHiRYK6ohKRGvDYRWrSpAlu3ryJQYMGYcyYMWjUqBHCwj6/5OnChQtwcHCALMsICgqCvb19NqUldeH6Sc2gs4XS+0I0jAwzPsXAwNgERrkLfNe1jQwNsDmIaymJNElMzN87OHCEUr+Zm5vjt99+g5+fH549e4bq1atjyZIlSEtL+8979+3bh6ZNm8La2hoBAQGoUKGCgMSUGU+ePEFYWBinuzWAzhZKn9AX3/xE99dKU8nwCXuRJdcmou/DQkkfa9SoEW7cuIGff/4Z48ePh6OjI0JDQ9Nf/+2339C5c2c4OTnh1KlTKFy4sMC09L18fX0BcP2kJtDJQvk2WYnoLH5wJjom6R/HNBKRWB8KJae86QNzc3MsW7YMZ8+excuXL1GjRg14enpi9OjRGDVqFMaNG4ft27dzWyAt5uPjgypVqsDCwkJ0FL2nk0cvPohJ/M8JOOomA4iKSYRtiXxZfCci+hqxsbEwNzeHmZmZ6CikYRo2bIgbN25g8uTJmDhxIgDAzc0Ns2fPFpyMMsvX1xetWrUSHYOgoyOUKcrsOSoxu+5DRF/GTc3pcxITE3H58mWYmpqiRIkS8PT0xMKFCz+5tpK0w6NHjxAREcHpbg2hkyOUpsbZ05OnuU6BXakCqFChQvpHqVKlYGiokz2dSKOxUFJGIiIi0KZNG7x58wbnzp1D1apVMX36dEyePBm7d+/Ghg0bUKVKFdEx6Rt9WD/ZuHFjsUEIgI4WyrKFzGEAZO20tyzj7dP72HL2KB4+fAhZ/vtupqamKFeu3D9K5oePcuXKIUeOHFmZikhvsVDSpwQGBqJ9+/YoXLgwgoKCUK7c3/sOe3p6onPnzhgwYABq1qyJWbNmYfz48TA21sl/FnWSj48PqlatynPWNYRO/s0xNzOGZcFceJCFD+aUKWwO31PHAQDJycmIiopCZGTkPz5OnTqFv/76C8nJyQAAAwMDlCxZMr1gli9f/h+Fkw8TEH2/2NhYFkr6h927d6Nv376oU6cO9u3b95/vsQ4ODrh27RpmzpwJV1dX7NmzBxs2bICtra2gxPQtfH194eTkJDoG/Y9OFkoAaGpjAa8LDz67ddCbKwehep+ItLd/b4j8LuIilAmvAAB5f2gPwxzmn/w8I0MDNK34/0+UmZmZwcbGBjY2Nv95r0qlwtOnT/9TNoODg7Fv3770zZgBIH/+/J8c2axQoQJKlizJqXSiz4iJiUHFihVFxyANIMsyli1bhnHjxqFHjx7YsGFDhrNDOXPmhIeHR/poZa1atTBjxgxMnDiRo5UaLDo6Gvfu3eP6SQ2is39b+tSzxMbAqM++582FvUh78//7SSaFBQBhAQCA3LZNMyyUaSoZfe0tvyqHoaEhSpYsiZIlS6JRo0b/eT0+Pv4/ZTMyMhKBgYF49OhR+lS6mZnZP6bSPx7d5FQ6Eae86W9paWkYM2YMfv/9d0yaNAnz5s37qh/G69Wrh6tXr2LWrFmYNm1a+milnZ1dNqSmb8X1k5rHQP7QWHSQy7oLCLgXo9YNzmVVGlIeBkMq/QZTpkxB/vz51Xbtf8toKj0yMhL37t3LcCr93x8FCnzfiUBE2sTc3Bxz587F6NGjRUchQZKSktC7d28cPHgQK1euxNChQ7/rOhcvXsSAAQMQHh6O6dOnY9KkSTAxMVFzWsqMAQMG4OrVq7hx44boKPQ/Ol0oH8YmoflSPySrcXsfM2NDOBlcxerF85AzZ05MmzYNv/zyC0xNTdV2j6+hUqnw5MmTT5bNyMhIxMXFpb+3QIF/Pon+8egmp9JJF7x//x45c+bEpk2bIEmS6DgkwIsXL9C+fXvcunUL27dvR7t27TJ1veTkZLi7u8PDwwPVqlXDxo0bUa1aNTWlpcwqV64cOnbsiGXLlomOQv+j04USALZdisbkPcFqu55HZzv0qGOJJ0+eYMaMGVi/fj3Kli2L+fPno1u3bjAwyPj88OwUFxeHe/fufbJsfm4q/d9PpXOTaNIGT548QcmSJXHo0CEu0tdDoaGhaNu2LZKSknDo0CH88MMParv25cuXMWDAAISGhsLNzQ1TpkzhaKVgUVFRKFeuHPbu3YtOnTqJjkP/o/OFEgBW+IRj0YmwTF9nQksbDG9q9Y9fCwkJwaRJk3D48GHUrVsXixYtgqOjY6bvlZXev3+f4VT6/fv3/zGVXqpUqQyfSudUOmmK4OBgVKtWDYGBgbC3txcdh7LRuXPn0LFjRxQtWhRHjx5FmTJl1H6P5ORkzJkzB/Pnz4ednR02bNiAGjVqqP0+9HU2btyIgQMH4tWrV9wdRYPoRaEE/h6pnHEgBEqV/E1rKo0MDWBsaAD3DrboUSfjB3F8fHwwfvx4XL16FR07dsSCBQtQqVIldUTPVpmZSv/4o0SJEpxKp2zj6+uLpk2bIjQ0lE9665GdO3fCxcUFDg4O2LNnT5b/kHvlyhUMGDAAd+7cgaurK6ZOnZrty50I6NevH27evIlr166JjkIf0ZtCCfy9pnLq3mD4R7yCkaHBZ4vlh9cdrQpjnrMdShfM9cXrq1QqbNu2DVOnTsWjR48wePBgzJw5E0WLFlXnlyFUXFxchmXz8ePH/5hK/zCi+e+RTU6lk7rt2bMHXbp0watXr/iktx6QZRmLFy/GhAkT0Lt3b6xfvz7bvqekpKRg7ty5mDdvHqpUqYKNGzeiZs2a2XJv+vu/fdmyZdGlSxcsWbJEdBz6iF4Vyg/CnyfA+0I0fMJeIDom6R8n6hgAsCyUC00rWqCvvSWsLPJ88/Xfv3+PFStWYO7cuVAqlZg4cSLGjh0Lc/NPb0OkK753Kv3fH1n55DzppjVr1uDnn39GamoqjIyMRMehLJSWloZRo0Zh5cqVcHV1xezZs4WsXb927RoGDBiAkJAQTJkyBW5ubhytzAb37t1DhQoVsH//fnTo0EF0HPqIXhbKjyUmKxEVk4gUpQqmxoYoW8gc5mbq2Z4zNjYWc+fOxYoVK1CoUCG4u7tjwIABevkPnkqlwuPHjzMc3YyPj09/b8GCBTNct8mpdPqUBQsWwNPTEzExMaKjUBZKTExEr169cOTIEaxatQqDBw8WmiclJQXz58/HnDlzULlyZWzYsEGtDwTRf61fvx6DBg1CbGwsBx80jN4Xyuxw//59uLq6YuvWrbC1tcXChQvRpk0bjXkiXBPExsZ+9qn0D3LkyJHhU+lly5blVLqemjBhAvbt24fw8HDRUSiLPHv2DO3bt8fdu3exY8cOtGnTRnSkdDdu3ED//v0RHByMyZMnY9q0afxelEVcXFxw+/ZtXLlyRXQU+hcWymx06dIlTJgwAX5+fmjatCk8PT350+xXeP/+Pe7fv5/hVHpKSgqAv6fSS5cuneHoJn+a1V0//fQTQkJCEBQUJDoKZYE7d+6gbdu2SE5OxuHDhzVyzWJqaioWLFiA2bNno2LFiti4cSNq164tOpZOkWUZlpaW6NGjBxYtWiQ6Dv0LC2U2k2UZhw8fxsSJE3Hnzh306dMHc+bMQdmyZUVH00ppaWmffSo9o6n0f38UL16cU+larFOnTkhNTcXhw4dFRyE1O3v2LDp27IiSJUviyJEjsLT8umNvRbl58yYGDBiAGzduYOLEiZgxYwZHK9UkIiIC1tbWOHjwYKY3rif1Y6EURKlUYsOGDZg+fTpiY2MxcuRITJ06lXs7qllsbOxnn0r/IEeOHP8Z0fzw/zmVrvkcHR1Rrlw5KBQK0VFIjbZu3Yr+/fujYcOG2L17t9bMMqSmpmLhwoWYNWsWrK2tsWHDBtStW1d0LK23du1a/Pzzz4iNjUW+fPlEx6F/YaEU7O3bt1i8eDE8PT1hamqKadOmYdiwYSww2eDdu3effSo9o6n0f3/wG5t4tra2aNGiBY9h0xGyLGPhwoWYPHkyJEnCmjVrtPIJ6lu3bqF///64du0aJkyYgJkzZyJHjhyiY2mtPn36IDw8HBcvXhQdhT6BhVJDPH36FDNnzsTatWtRpkwZzJs3D927d+c0rCBpaWmffSr99evX6e8tVKjQf0rmh9FNTqVnj2LFimH48OGYNm2a6CiUSUqlEr/++itWr16N6dOnY+bMmVr9AKNSqYSnpydmzpyJ8uXLY8OGDTzN6TvIsoxSpUqhT58+WLhwoeg49AkslBrmzp07mDRpEg4ePIg6derA09MTjRs3Fh2LPiLL8mefSv/SVPrHT6Vr46iLppFlGaampli+fDmGDRsmOg5lwtu3b9GjRw+cOHECq1evxsCBA0VHUpuQkBAMGDAAV65cwbhx4zBr1izkzJlTdCytERYWBhsbGxw5ckSjnvCn/8dCqaH8/PwwYcIEXLp0Ce3bt4eHhwcqV64sOhZ9hXfv3n32qfTU1FQAgKGh4Sen0j8UUE6lf503b94gX7582LZtG3r06CE6Dn2np0+fol27dggPD8euXbvQsmVL0ZHUTqlUYvHixZg+fTrKlSuHDRs2wMHBQXQsrfDXX39h2LBhiIuLQ548337gCGU9FkoNplKpsGPHDkydOhXR0dEYNGgQZs6ciWLFiomORt8pLS0Njx49ynB080tT6R8/la7N04DqFBUVhXLlyuHEiRNo0aKF6Dj0HW7fvo02bdogLS0Nhw8fRvXq1UVHylK3b9/GgAEDcOnSJYwdOxazZ8/maOUX9OrVC/fv3+fWYBqMhVILJCcn448//sDs2bORkpKCCRMmYNy4ccidO7foaKRGH6bSM1q3+eTJk/T35syZ8x9T6R//b32bSr9y5Qpq166NK1euoFatWqLj0Dfy8fGBs7MzLC0tceTIEZQqVUp0pGyhVCqxdOlSTJs2DWXKlMH69evRoEED0bE0kizLKF68OPr3748FCxaIjkMZYKHUInFxcZg3bx5+++03FCxYELNmzcLAgQNhbKyeoyJJs7179w737t375Ojm10ylf/jImzev4K9EvU6cOIFWrVohKioKZcqUER2HvsHmzZsxcOBANG7cGLt27dLLZR53797FgAEDcOHCBYwePRpz5sxBrly5RMfSKHfv3kXlypVx7NgxtGrVSnQcygALpRaKioqCm5sbvL29UblyZXh4eKBdu3acAtVjH6bSMxrdfPPmTfp7Cxcu/Mk1m9o6lb5161b07t0bb9684doqLSHLMubNmwc3Nzf0798ff/31F0xMTETHEiYtLQ3Lli2Dm5sbSpUqhfXr18PR0VF0LI2xatUqjBw5EnFxcZyZ02AslFrsypUrmDBhAnx8fNC4cWMsWrSIR33Rf2RmKv3jjzJlymjkVPrKlSsxZswYJCcna10Z1kepqakYNmwY1q5di1mzZmHatGn87/Y/oaGhGDhwIAIDAzFy5EjMnTsX5ubmomMJ16NHDzx8+BABAQGio9BnsFBqOVmWcfToUUycOBEhISHo2bMn5s2bh3LlyomORloiKSkpw6fSo6Ki/jGVbmlpmeHopqipdHd3d6xatQpPnz4Vcn/6egkJCejWrRtOnz6NtWvXol+/fqIjaZy0tDT89ttvmDp1KkqWLIn169ejUaNGomMJI8syihUrhp9++gnz5s0THYc+g4VSRyiVSmzcuBHTp09HTEwMfv31V7i6uqJgwYKio5EWy8xU+scfxYoVy7JRqFGjRuH06dO4detWllyf1OPJkydwcnLCvXv3sGfPHvz444+iI2m08PBwDBw4EOfOncOIESMwf/58vRytvH37NmxtbbmLgxZgodQxiYmJWLJkCRYuXAhjY2O4urri119/5XFfpHayLCMmJibDsvnxiGGuXLk+e1Z6ZtbP9e3bF9HR0Th79qw6vizKArdu3ULbtm0hyzKOHDkCOzs70ZG0gkqlwu+//44pU6agePHiWLduHZo0aSI6Vrb6sKQlLi5OLwu1NmGh1FHPnz/HrFmz8Ndff6FUqVKYN28eevbsyWMAKdtkZir9448vPWjTtm1bmJmZYe/evdnxZdE3On36NDp37oxy5crh8OHDKFmypOhIWiciIgIDBw6Ev78/hg0bBg8PD715OKVbt254+vQpzp07JzoKfQELpY67e/cuJk+ejP379+OHH36Ap6cnmjZtKjoW6bm0tDQ8fPgww9HNhISE9PcWKVIkw9OEihUrBnt7e9jZ2WHt2rUCvyL6FIVCgZ9++gk//vgjdu7cyafwM0GlUmHlypWYPHkyLCwssG7dOjRr1kx0rCylUqlQtGhR/Pzzz5gzZ47oOPQFLJR6wt/fHxMmTMCFCxfg5OQEDw8P2Nraio5F9B+yLOPVq1eIjIz85J6b/55KVyqVsLS0RIcOHf7zVLo+b0UjkizLmD17NmbMmIFBgwbhjz/+4H8LNYmMjMRPP/0EPz8//PLLL/Dw8NDZon7r1i3Y2dnh1KlTXHOrBVgo9Ygsy9i5cyemTJmCqKgoDBw4EO7u7ihevLjoaERfLSkp6R9Fc+rUqbC0tIQsy4iKioJSqQQAGBkZZTiVXr58eZ39R1i01NRUDBkyBBs3bsScOXMwdepUbgukZiqVCqtWrcKkSZNQuHBhrFu3TicL1++//45x48YhPj6em71rARZKPZSSkoJVq1bB3d0d79+/x/jx4zF+/Hj+A0taJy0tDcbGxlizZg0GDRoEpVKZPpX+qdHNL02lf/goWrQoS9B3ePPmDbp27QpfX1+sX78effv2FR1Jp927dw+DBg2Cj48Pfv75ZyxcuFCnTsLq0qULXr58yQfutAQLpR6Lj4/H/PnzsXz5cuTPnx8zZ87EoEGDeJQjaY1Xr16hSJEi2LNnD5ydnT/73o+n0j/18ezZs/T3mpubZ/hUOqfSP+3Ro0dwcnLCgwcPsHfvXq7VziYqlQqrV6/GhAkTUKhQIaxdu1YnttdRqVQoUqQIhg8fDnd3d9Fx6CuwUBKio6Ph5uaGzZs3o2LFivDw8ECHDh04QkMaLzQ0FJUqVYKfn1+mN39OTEzM8Kz0r51Kr1Chgt48ffuxGzduwMnJCUZGRjhy5AjXZwsQFRWFn376CWfOnMHgwYPh6emp1Wej37x5E9WrV8eZM2f4w4mWYKGkdNeuXcOECRNw+vRpODo6YtGiRahbt67oWEQZCgwMRP369REcHIyqVatm2X0+nkr/1Mfbt2/T32thYZHhU+m6OJV+4sQJdO3aFdbW1jh06BDXZAskyzLWrFmDcePGIX/+/Fi7di1atWolOtZ3Wb58OSZOnIj4+HjkzJlTdBz6CiyU9A+yLOP48eOYOHEigoOD0aNHD8ybNw/ly5cXHY3oPw4dOoT27dvjyZMnwoqMLMt4+fLlJ0c2v2Yq/cOHpaWl1k2lb9iwAUOGDEHLli2xfft2vRyd1UQPHjzA4MGDcfLkSfz0009YvHix1o1WOjs7Iy4uDr6+vqKj0FdioaRPSktLg0KhgJubG16+fInhw4fDzc0NhQoVEh2NKN2mTZvQv39/vH//HmZmZqLjfNKHqfRPlc0HDx78Yyq9TJkyGY5ualJZk2UZM2fOhLu7O37++WesWLGCa681jCzLWLduHcaOHYu8efNizZo1aNOmjehYX0WlUqFw4cIYOXIkZs6cKToOfSUWSvqspKQkLF26FB4eHjA0NMTUqVMxcuRIHuVIGmHJkiWYPn36P6actYlSqUR0dHSGhfNLU+kfPiwsLLJtKj0lJQWDBw+GQqHAggULMHHiRJ2bxtcl0dHRGDJkCI4fP44BAwZgyZIlyJ8/v+hYn3X9+nXUrFkTvr6+aNy4seg49JVYKOmrvHjxAu7u7li9ejVKlCiBOXPmoE+fPjzKkYRyc3ODl5cXHjx4IDqK2n2YSs9o3ebz58/T35s7d+5PTqWXL18eZcqUUdvoYXx8PLp06YJz585h48aN6NWrl1quS1lLlmVs2LABY8aMQe7cufHXX3/ByclJdKwMLV26FFOmTEF8fDwHL7QICyV9k7CwMEyZMgV79uxBzZo14enpqZMb6pJ2+OWXX3DhwgVcvXpVdJRs9/bt288+lZ6WlgYg46n0D4Xza6fSo6Oj0bZtWzx+/Bj79+/P9FP1lP0ePXqEwYMH49ixY5AkCcuWLUOBAgVEx/qPjh07IiEhAWfOnBEdhb4BCyV9l/Pnz2PChAkIDAxE69atsXDhQtjZ2YmORXqme/fuiIuLw8mTJ0VH0SgfptIzGt1MTExMf2/RokUzLJsfptKvXbsGJycnmJmZ4ciRI6hcubLAr44yQ5ZlbNq0CaNHj0auXLmwevVqtG/fXnSsdGlpaShUqBDGjh2L6dOni45D34CFkr6bLMvYvXs3Jk+ejPv376N///5wd3dHyZIlRUcjPfHjjz+icOHC2L59u+goWkOWZbx48SLD04T+PZVeuHBhPHr0CIUKFcKYMWNQq1at9KfS+SCO9nr8+DGGDBmCI0eOoG/fvli+fDkKFiwoOhauXr2KH374AWfPnoWjo6PoOPQNWCgp01JSUrB69WrMmjULSUlJGDt2LCZOnKhTR4CRZqpZsyYcHBzwxx9/iI6iMz5MpUdGRmLLli3YvXs3ChUqhNy5c+Phw4fpU+nGxsafnUo3NzcX/JXQl8iyDC8vL4waNQo5cuTAn3/+iY4dOwrNtHjxYri5uSE+Pl5jd26gT2OhJLV5/fo1PDw8sHTpUuTJkwczZ87E4MGDtW5vPdIelpaW6NevH2bPni06ik6RZRlubm6YN28ehg0bhuXLl8PY2BipqanpU+mfGt38mqn0ChUqoEiRInwyXIM8efIEP//8Mw4dOoTevXvjt99+E7ZFXPv27fHu3TucOnVKyP3p+7FQkto9fPgQ06ZNg0KhgLW1NRYsWIBOnTrxHxBSO3Nzc8ydOxejR48WHUVnJCcn46effoK3tzc8PT0xbty4r/q7+/FU+qc+Xrx4kf7e3LlzZ1g2S5cuzal0AWRZhre3N0aOHAlTU1OsWrUKzs7O2ZpBqVSiUKFCmDBhAtzc3LL13pR5LJSUZW7cuIGJEyfixIkTaNCgARYtWgR7e3vRsUhHvH//Hjlz5sSmTZsgSZLoODohLi4OnTt3RmBgIBQKBbp37662ayckJGT4VPqDBw84la4hnj59iqFDh+LAgQPo2bMnfv/9dxQuXDhb7n358mXUqVMH586dQ4MGDbLlnqQ+LJSU5U6cOIEJEybg5s2b6Nq1K+bPnw8rKyvRsUjLPXnyBCVLlsShQ4c0ek89bfHgwQO0adMGz58/x/79+9GwYcNsu/fHU+mf+khKSkp/b7FixTI8TYhT6eohyzK2bt2KESNGwNjYGH/88Qe6dOmS5ff19PTEzJkzERcXB1NT0yy/H6kXCyVli7S0NGzevBlubm54/vw5fvnlF0ybNi3bfvIl3RMcHIxq1aohMDCQI9+ZdOXKFbRr1w45c+bE0aNHYWNjIzpSOlmW8fz58wzXbX48lZ4nT54Mz0rnVPq3e/bsGX755Rfs27cP3bt3x4oVK1CkSJEsu5+TkxNSU1Nx4sSJLLsHZR0WSspW7969w7JlyzB//nwYGBhgypQpGDVqFHLmzCk6GmkZX19fNG3aFGFhYbC2thYdR2sdPnwY3bt3R9WqVXHw4EFYWFiIjvRNPkylf2pkMzo6+h9T6WXLlv1k2SxXrhyn0jMgyzK2b9+OX3/9FYaGhli5ciW6deum9vsolUoULFgQkydPxtSpU9V+fcp6LJQkxMuXLzF79mysWrUKxYoVw5w5c9C3b18YGRmJjkZaYvfu3ejatStevXol7IlUbffnn39i+PDhaN++PbZs2YJcuXKJjqRWqampePDgQYajm1+aSv/wUbhwYb2fSn/+/DmGDRuGPXv2oGvXrli5cqVaf/i4ePEi6tWrh4CAADg4OKjtupR9WChJqPDwcEydOhW7du1C9erV4enpiRYtWoiORVpgzZo1+Pnnn5GamsofRL6RSqXC1KlT4eHhgREjRmDp0qV693v48VT6pz5evnyZ/t48efJkWDZLlSqlN1Ppsixj586dGD58OABgxYoV6N69+3eV7cRkJaJiEpGiVMHU2BB7FX9hwZxZiIuL41ZzWoqFkjRCYGAgxo8fj4CAALRq1QoLFy5EtWrVRMciDbZgwQJ4enoiJiZGdBStkpycjP79+2P79u1YvHgxRo8erfejb5+SkJCQXi7/Pbr54MEDqFQqAJ+fSi9fvrzOjfoCwIsXL/Drr79i586d6Ny5M/744w8ULVr0i58X/jwB3hei4RP6AtGxSfhH+ZBlmKS8Rp8mNdCnniWsi+bJsvyUNVgoSWPIsox9+/Zh0qRJiIiISN+wulSpUqKjkQaaMGEC9u3bh/DwcNFRtEZsbCw6deqES5cuYfPmzdny5K4u+ngq/d8f9+7d+8dUevHixTMc3SxUqJBWl/kPo5VpaWlYsWIFevbs+cmv52FsEqbuDYZ/xCsYGRogTZVx7fjwuqNVYcxztkPpgrpXyHUVCyVpnNTUVPz111+YNWsWEhISMGbMGEyaNAn58uUTHY00yMCBA3H79m0EBQWJjqIV7t+/jzZt2uDVq1c4ePAg16llEVmW8ezZswxHNz+eSs+bN+9nn0rXhmUIL1++xIgRI7B9+3Z06tQpfV38B9suRWPGgRAoVfJni+S/GRkawNjQALM62KJnHcusiE5qxkJJGuvNmzdYuHAhlixZAnNzc8yYMQNDhgzh/mQEAOjUqRNSU1Nx+PBh0VE03qVLl9CuXTvkzZsXR44c4VPxAr158+azT6V/mEo3MTH57FS6pu2MsXv3bgwbNgypqan4/fff0bt3b6z0jcCiE2GZvvb4lhXxa1P+mdV0LJSk8R49eoQZM2Zgw4YNqFChAhYsWIDOnTtr9VQRZZ6joyPKlSsHhUIhOopGO3DgAHr16oVq1arhwIEDWbqPIGVOSkrKZ59Kf/fuXfp7NXEq/dWrVxg5ciS2bt2KBtIEPCrRWG3X9uhshx4cqdRoLJSkNYKDgzFx4kQcO3YM9evXh6enJ+rXry86FglSpUoVtGzZEsuWLRMdRWOtXLkSI0eORKdOnbB582aNG9Wir/fvqfR/f7x69Sr9vXnz5v3sU+lZPZW+dttezL4CwMjkP8X2/YObeL710/tMFnNZBLOSlT75mpmxIU6Nacw1lRpMP/Y6IJ1gZ2eHo0eP4tSpU5gwYQIaNGiAzp07Y8GCBZzC00OxsbHcfzIDKpUKEydOxOLFizFmzBh4enpqxXo8ypiBgQGKFy+O4sWLf/JYzDdv3nxyZHP79u3ZPpXul1gCxiavkPaZ4ao8P7SHafGK//g14wLFM3y/UiVj6t5geP1UL9P5KGuwUJLWad68Oa5cuQJvb2+4urqiSpUqGDp0KKZPn87pPD0hyzJiYmJYKD/h/fv3kCQJu3btwvLlyzFy5EjRkSgb5M2bFzVr1kTNmjX/89rHU+kff/j6+mL9+vX/mEovUaJEhqObBQsW/OJUevjzBPhHvPrsewDArLQtzCt9/XnxaSoZ/hGvEPEiAVYW3FJIE7FQklYyNDSEi4sLunbtit9//x3z5s3Dpk2bMHnyZIwePVon936j/5eQkAClUslC+S8xMTHo2LEjrly5gt27d8PZ2Vl0JNIApqamsLa2/uRMjizLePr06X9GN+/cuYNDhw79Yyo9X758GT6V/mEq3ftC9Be3BvpAlZwEAxMzGBh+3ei5kaEBNgdFY2YH26//4inbcA0l6YRXr15hzpw5+OOPP2BhYYHZs2dDkiRO8+mo+/fvo3z58jhx4gRPVvqfyMhItGnTBnFxcTh48CDs7e1FRyId8Pr16wyfSn/48GH6VLqpqSnKli0LldN0pJrlz/B6H9ZQGpjmhJzyDjAwhFlpWxRoOhBmxb+8dKlMoVzwG99UXV8eqRELJemUyMhITJ06FTt27ICdnR08PT3RqlUr0bFIza5cuYLatWvjypUrqFWrlug4wl24cAHt27dH/vz5cfToUVSoUEF0JNIDKSkpiIqKSh/dvBsZhYOmjYHPTIu/f3QHCZf2Imf52jDMlQ+pr6Lx5uJeyKnvUayvJ0yLff7PrgGAWzNbwdyME6yahoWSdNKFCxcwfvx4nDt3Di1atMDChQtRo0YN0bFITU6cOIFWrVohKioKZcqUER1HqH379qF3796oVasW9u/fz2UAJEzIk9dw+v3cN39eatwTPF03AmalbVG0h/sX3394REPYluBBF5rGUHQAoqxQr149nD17Fvv27UN0dDRq1aqFfv36ITo6WnQ0UoMP53fre3n67bff0LlzZ7Rr1w6nTp3S+98PEitFqfquzzMpUAI5revhffRNyKq0LLsPZS0WStJZBgYG6NixI4KDg7Fy5UocO3YMFStWxOTJk/H69WvR8SgTYmJiYGJiAnNzc9FRhFCpVBg7dixGjRqFcePGYdu2bciRI4foWKTnTI2/v1IY5y0MpCkhpyZn6X0o6/C/Cuk8ExMT/PLLL4iIiMDEiRPx+++/o0KFCli+fDlSUlJEx6Pv8GEPSn08Lendu3fo1q0bli9fjhUrVsDT0xOGhvxWTuKVLWSO7/0bqYx/BgNjUxiYfv4HI4P/3Yc0D78Lkd7IkycP3N3dER4eDmdnZ4wdOxZVqlTBzp07waXE2kVf96B8+fIlmjVrhqNHj2Lv3r0YPny46EhE6czNjGH5hZNs0pL+OzuU8vweksIvIkfZmjAw+HwtsSyUiw/kaCgWStI7JUqUwJo1a3Djxg3Y2Nige/fucHBwwLlz376YnMSIiYlBwYIFRcfIVuHh4XBwcMC9e/fg5+eHDh06iI5E9B9NbSxgZJjxOOXLfR54sXMmXgdsR8L1Y4g9tQbPNk+AgYkZCjTp/9lrGxkaoGlFCzUnJnVhoSS9VbVqVRw+fBinT59GamoqHB0d4ezsjNDQUNHR6Av0bYQyICAADg4OMDY2RlBQEOrUqSM6EtEn9aln+dlNzXNVtEda0hu8ubgPsSdWIemuP3JVrI/i/ZfCpHDpz147TSWjr72luiOTmnDbICL8/ZDD1q1bMXXqVDx+/BhDhgzBjBkzULRoUdHR6BPq1asHOzs7rF27VnSULLd792706dMHdevWxb59+/RuZJa0j8u6Cwi4F/NVp+V8LSNDA9QvX4hneWswjlAS4e+jHPv06YPQ0FAsWLAAW7duhZWVFebMmYPExETR8ehf9GGEUpZlLF26FN26dYOzszNOnDjBMklaYZ6zHYw/M+39PYwNDTDP2U6t1yT1YqEk+kiOHDkwfvx4REREYPDgwXB3d0fFihWxbt06pKV9eX80yh66voYyLS0No0aNwtixYzFx4kR4e3tzWyDSGqUL5sIsNZ+37d7BFqW/8MAPicVCSfQJhQoVwpIlSxAaGopGjRph0KBBqFGjBo4ePconwgVTKpWIj4/X2RHKpKQkdOnSBStXrsSff/6JBQsWcFsg0jo961hifMuKarnWhJY26FGHayc1Hb9LEX1GuXLlsHXrVly8eBEFCxZE27Zt0bx5c1y9elV0NL0VHx8PQDdPyXnx4gWaNm2KU6dO4cCBA/j5559FRyL6br82tcaCznYwMzb87JPfn2JkaAAzY0N4dLbD8KZWWZSQ1ImFkugr1KlTB76+vjhw4ACePHmCH374AS4uLnjw4IHoaHpHV49dDA0NhYODA6Kjo+Hn5wcnJyfRkYgyrWcdS5wa0xj1y//99/VLxfLD6/XLF8KpMY05MqlF+JQ30TdSKpVYt24dZsyYgfj4eIwcORJTpkxBgQIFREfTCwEBAWjQoAGCg4NRtWpV0XHU4ty5c+jYsSOKFi2Ko0ePokyZMqIjEald+PMEeF+Ihk/YC0THJOHj8mGAvzctb1rRAn3tLWFlkUdUTPpOLJRE3+nt27dYtGgRPD09kSNHDri5uWHYsGEwMzMTHU2nHTp0CO3bt8eTJ09QvHhx0XEybceOHZAkCQ4ODtizZw9/MCG9kJisRFRMIlKUKpgaG6JsIXOegKPlOOVN9J1y586NmTNnIiIiAl27dsX48eNRuXJlbN++nQ/uZCFdmfKWZRmenp7o0aMHunTpgmPHjrFMkt4wNzOGbYl8qGlZALYl8rFM6gAWSqJMKl68OFavXo3g4GDY2tqiZ8+eqFevHs6ePSs6mk6KiYlB7ty5YWpqKjrKd1Mqlfj1118xceJEuLq6YvPmzRzZJiKtxkJJpCZVqlTBwYMH4ePjA5VKhcaNG6Njx464c+eO6Gg6Rdv3oExMTISzszNWr16Nv/76C3PmzIGBgXo3gSYiym4slERq1qRJE1y8eBFbtmzBzZs3YWdnh6FDh+LZs2eio+mE2NhYrZ3ufvbsGZo0aQJfX18cPHgQgwcPFh2JiEgtWCiJsoChoSF69eqFu3fvYuHChdixYwesrKwwa9YsvH37VnQ8raatxy7euXMHDg4OePz4Mfz9/dGmTRvRkYiI1IaFkigLmZmZYezYsYiMjMQvv/yCefPmwdraGmvWrIFSqRQdTytp45T32bNnUb9+fZibmyMoKAg1atQQHYmISK1YKImyQYECBeDp6YnQ0FA0a9YMQ4YMQfXq1XHo0CE+Ef6NtG2EcuvWrWjRogVq1aqFc+fOwdKSGzUTke5hoSTKRmXLloW3tzcuXboECwsLtG/fHs2aNcPly5dFR9Ma2rKGUpZlLFiwAL1790bPnj1x9OhR5M+fX3QsIqIswUJJJEDt2rVx5swZHDp0CC9evECdOnXQu3dvREVFiY6m8bRhhFKpVGLo0KGYMmUKpk+fjo0bN2r1NkdERF/CQkkkiIGBAZycnHDjxg2sWbMGvr6+sLGxwfjx4xEbGys6nkZ6//49kpKSNHoN5du3b9GxY0esX78e69atw6xZs7gtEBHpPBZKIsGMjY0xaNAghIeHw9XVFX/++SesrKywePFiJCcni46nUTT9lJynT5+icePG8Pf3x+HDhzFw4EDRkYiIsgULJZGGMDc3x/Tp0xEZGYkePXpg0qRJqFSpErZu3QqVSiU6nkb4MHKriYUyJCQE9vb2eP78Ofz9/dGyZUvRkYiIsg0LJZGGKVq0KFatWoVbt26hWrVq6N27N+rVqwdfX1/R0YTT1BFKHx8fNGjQAPny5UNQUBCqV68uOhIRUbZioSTSUJUqVcL+/fvh5+cHQ0NDNG3aFO3bt8ft27dFRxPmQ6HUpDWUmzdvRqtWrVC3bl34+/ujVKlSoiMREWU7FkoiDdeoUSMEBQVh27ZtCAkJgZ2dHYYMGYKnT5+KjpbtYmJiYGBgoBHb78iyjLlz58LFxQV9+/bF4cOHkS9fPtGxiIiEYKEk0gIGBgbo0aMH7ty5g8WLF2P37t2wsrLCjBkz9Ooox9jYWBQoUABGRkZCc6SmpmLIkCFwc3ODu7s71q1bBxMTE6GZiIhEYqEk0iJmZmYYPXo0IiMj8euvv8LDwwNWVlZYvXq1XhzlqAl7UL558wbt27fHxo0bsXHjRkybNo3bAhGR3mOhJNJC+fPnh4eHB0JDQ9GiRQsMHToUdnZ2OHDggE4f5Sj6HO/Hjx+jUaNGCAwMxLFjx9CvXz9hWYiINAkLJZEWK1OmDLy8vHDlyhWUKFECHTt2RJMmTXDp0iXR0bKEyBHK4OBg2NvbIyYmBufOncOPP/4oJAcRkSZioSTSAbVq1cKpU6dw5MgRxMbGom7duujZsyfu3bsnOppaiTrH+/Tp02jYsCEKFSqEoKAg2NnZZXsGIiJNxkJJpCMMDAzQpk0bXL9+HevWrYO/vz8qVaqEMWPGpG+3o+1ETHlv2rQJrVu3hoODA/z9/VGyZMlsvT8RkTZgoSTSMUZGRhg4cCDCwsIwY8YMrF27FhUqVICnpyfev38vOl6mZOeUtyzLcHd3R//+/dG/f38cPHgQefLkyZZ7ExFpGxZKIh1lbm4OV1dXREZGok+fPpgyZQoqVaoEb29vrTzKUZblbJvyTk1NxcCBAzFjxgzMmTMHf/31F7cFIiL6DBZKIh1nYWGBlStXIiQkBDVr1kTfvn1Rp04dnDlzRnS0b5KQkAClUpnlhfLNmzdwcnKCt7c3Nm/eDFdXV24LRET0BSyURHrCxsYGe/fuhb+/P0xNTfHjjz+ibdu2uHXrluhoXyU7jl189OgRHB0dcfHiRRw/fhx9+vTJsnsREekSFkoiPdOwYUMEBARg586dCAsLQ/Xq1TFo0CA8fvxYdLTP+lAos2qE8saNG7C3t0d8fDzOnz+Ppk2bZsl9iIh0EQslkR4yMDBA165dcfv2bSxduhT79u2DtbU1pk2bhoSEBNHxPik2NhZA1hTKEydOwNHREUWLFkVQUBBsbW3Vfg8iIl3GQkmkx0xNTTFy5EhERkZi1KhRWLRoEaysrLBq1SqkpqaKjvcPWTVCuX79erRt2xaOjo7w8/ND8eLF1Xp9IiJ9wEJJRMiXLx/mz5+P0NBQtG7dGsOHD4ednR327dunMUc5xsTEwMTEBObm5mq5nizLmD59On766ScMGjQI+/fvR+7cudVybSIifcNCSUTpLC0tsWnTJly9ehWWlpZwdnZGo0aNEBQUJDpa+h6U6njiOiUlBf369cPs2bOxYMECrFq1CsbGxmpISUSkn1goieg/atSogRMnTuDYsWN4/fo1HBwc0L17d0RGRgrLpK49KOPj49GmTRts374dW7ZswaRJk7gtEBFRJrFQElGGWrVqhWvXrmHDhg0ICAhA5cqVMWrUKLx69Srbs6jjlJzo6Gg0bNgQV69excmTJ9GrVy81pSMi0m8slET0WUZGRujfvz/CwsIwa9YsbNiwAVZWVvDw8MC7d++yLUdmz/G+du0a7O3tkZiYiICAADRq1EiN6YiI9BsLJRF9lVy5cmHKlCmIjIyEi4sL3NzcYGNjA4VCkS1HOWZmhPLo0aNo1KgR/q+9e4tt6j7gOP47thM3yRJVuXY1eAi5CRGN9oAKaA2dookKIQXVi8omwQpiWnipJkV0QLksXKSIiIoXJlWCVQxKEdrYPNpKSCVbkCpYUtQqIo3aglWBSZmckqSFOMSJ7bOHjrYs2Ln8fcnE9/N6fP7n+OXoa5/z/x+Px6Ouri7V1tam+ewA4NFGUAKYkYqKCh0+fFh9fX1aunSpNmzYoCVLlqijoyOjx53tM5RHjx5VY2OjGhoadOHCBVVVVWXg7ADg0UZQApiV6upqnTlzRhcvXlRBQYFWrlypVatW6cqVK2k9TiQaU9+tr/WV83ElSp5UJBqb1n62bWvnzp1qbm7W5s2bFQgE0rbkEADgQZY9VxaZA/B/y7ZtBQIBbd++XcFgUBs3btT+/fvl8XhmNd618F291R1S52cDCg2N6vsXKUuSt7RQDTWVWrfMq6eqiiftH41GtWnTJp06dUoHDx7Uli1bmMkNABlEUAJIm4mJCR05ckR79uxRJBJRS0uLtm3bppKSkmntf3NoVDsCvXo/eFtOh6V4Ivnl6f72Fb5ytfnrNL+0UJI0PDwsv9+vrq4unThxQmvXrk3LdwMAJEdQAki7O3fuqL29XYcOHVJxcbFaW1vV3NysvLy8pPucvhxS69t9iiXslCH5v5wOSy6Hpb1rFmt5RUKrV69WOBzW2bNnVV9fn46vAwCYAkEJIGP6+/u1e/duHT9+XD6fTwcOHJDf7590+/kPndf02ntXjY838eHf5A526ty5c6qpqTEeDwAwPUzKAZAx8+bN07Fjx9TT06OFCxeqqalJ9fX1unTp0refOX05lJaYlKS8JT/Xq398h5gEgCzjH0oAWXP+/Hlt3bpVPT09ampq0m937NNvAjcUjU1exzL676uK9P5DY6Fexb4Oy1FQIveTNXr8uV8przT5ZB+3y6GOlp9++0wlACDzCEoAWZVIJHTy5Ent2rVL4882q+BHP5ZtTb5Z8mWgTdH+T1S4qF55lQsUHxnW3Y/elT0+pideek35FQseOr7TYeknC8v05q+XZfibAADuIygB5ERv6LYaX+9Oun2s/xO5f+iT5fxuIs/E0Be69cbLKlr0rMobX0k5fkfLc/JVTl5SCACQfjxDCSAn/toTltORfG3Ix+bVPhCTkpRX6lF+uVcTt2+mHNvpsHSyK5SW8wQATI2gBJATnZ8NzGh5IOmbBdTjo1/JUZh6Xct4wlbn1QGT0wMAzABBCSDrRqIxhYZGZ7xfpO+C4ncHVbRoxZSfDQ2OTvs1jQAAMwQlgKy7MRjRTB/enhi8qaHzr8vtWaSiup9N+Xlb0vXByKzODwAwMwQlgKwbf8gyQanER4Y18Je9criLVP7Cq7IczowcBwAwO65cnwCAR0++a/q/ZRNjEYX/3KrEWERV69vlKi7LyHEAALPH1RZA1i0oK1Ly+d3fsWPjGjizT7HhL1T54u+VX+6d9jGs/x4HAJB5BCWArCtyu+Sd4k02diKuL//eruitT1Xxwna5PbUzOoa3rFBFbm7CAEA2cLUFkBMNNZV6s/tG0qWDhv/5hu4Fu1XgW6r4vRGNfNz5wPYfPN2QdGynw1JDdWVazxcAkBxBCSAn1i3z6k//up50+3j4c0nSveAHuhf8YNL2VEEZT9hav3z6t8cBAGYISgA58VRVsVb4ynXp88GH/kv5xLoDsxr3/ru8ee0iAGQPz1ACyJk2f51cKV6/OBsuh6U2f11axwQApEZQAsiZ+aWF2rtmcVrH3LdmseZPMeEHAJBeBCWAnPrlM1698nx1Wsb63fM1+sUzPDsJANlm2bY90zegAUDanb4cUuvbfYol7KQzvx/G6bDkcljat2YxMQkAOUJQApgzbg6NakegV+8Hb8vpsFKG5f3tK3zlavPXcZsbAHKIoAQw51wL39Vb3SF1Xh1QaHBU379IWfpm0fKG6kqtX+5lNjcAzAEEJYA5LRKN6fpgROOxhPJdDi0oK+INOAAwxxCUAAAAMMIsbwAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABj5D742zA/EXDkvAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import networkx as nx\n", + "\n", + "from qiskit_optimization.applications import Maxcut\n", + "\n", + "seed = 1\n", + "num_nodes = 6\n", + "graph = nx.random_regular_graph(d=3, n=num_nodes, seed=seed)\n", + "nx.draw(graph, with_labels=True, pos=nx.spring_layout(graph, seed=seed))\n", + "\n", + "maxcut = Maxcut(graph)\n", + "problem = maxcut.to_quadratic_program()\n", + "print(problem.prettyprint())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Encode the problem into a quantum Hamiltonian\n", + "\n", + "Once we have appropriately configured our problem, we proceed to encode it using the `QuantumRandomAccessEncoding` class from the `qrao` module. This encoding step allows us to generate a quantum Hamiltonian operator that represents our problem. In particular, we employ a Quantum Random Access Code (QRAC) to encode multiple classical binary variables (corresponding to the nodes of our max-cut graph) into each qubit.\n", + "\n", + "It's important to note that the resulting \"relaxed\" Hamiltonian, produced by this encoding, will not be diagonal. This differs from the standard workflow in `qiskit-optimization`, which typically generates a diagonal (Ising) Hamiltonian suitable for optimization using a `MinimumEigenOptimizer`. You can find a tutorial on the `MinimumEigenOptimizer` [here](https://qiskit.org/documentation/optimization/tutorials/03_minimum_eigen_optimizer.html).\n", + "\n", + "In our encoding process, we employ a $(3,1,p)-$QRAC, where each qubit can accommodate a maximum of 3 classical binary variables. The parameter $p$ represents the bit recovery probability achieved through measurement. Depending on the nature of the problem, some qubits may have fewer than 3 classical variables assigned to them. To evaluate the compression achieved, we can examine the `compression_ratio` attribute of the encoding, which provides the ratio between the number of original binary variables and the number of qubits used (at best, a factor of 3)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Our encoded Hamiltonian is:\n", + "( SparsePauliOp(['XX', 'XY', 'XZ', 'YX', 'ZX', 'YY', 'YZ', 'ZY', 'ZZ'],\n", + " coeffs=[1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j,\n", + " 1.5+0.j]) ).\n", + "\n", + "We achieve a compression ratio of (6 binary variables : 2 qubits) ≈ 3.0.\n", + "\n" + ] + } + ], + "source": [ + "from qiskit_optimization.algorithms.qrao import QuantumRandomAccessEncoding\n", + "\n", + "\n", + "# Create an encoding object with a maximum of 3 variables per qubit, aka a (3,1,p)-QRAC\n", + "encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3)\n", + "\n", + "# Encode the QUBO problem into an encoded Hamiltonian\n", + "encoding.encode(problem)\n", + "\n", + "# This is our encoded Hamiltonian\n", + "print(f\"Our encoded Hamiltonian is:\\n( {encoding.qubit_op} ).\\n\")\n", + "print(\n", + " \"We achieve a compression ratio of \"\n", + " f\"({encoding.num_vars} binary variables : {encoding.num_qubits} qubits) \"\n", + " f\"≈ {encoding.compression_ratio}.\\n\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solve the problem using the `QuantumRandomAccessOptimizer`\n", + "\n", + "Having successfully encoded our input problem as a relaxed Hamiltonian, we proceed to solve it using the `QuantumRandomAccessOptimizer`. This optimizer allows us to find an approximate solution to the relaxed problem by leveraging quantum computing techniques.\n", + "\n", + "To set up the optimizer, we need to specify two crucial components:\n", + "\n", + "1. **Minimum Eigensolver**: We specify a minimum eigensolver to heuristically search for the ground state of the relaxed problem Hamiltonian. As an example, we can use the Variational Quantum Eigensolver (VQE). For simulation purposes, we'll employ an simulator, but you can choose a quantum device as the backend if desired.\n", + "2. **Rounding Scheme**: To map the ground state results back to a solution for the original problem, we specify a rounding scheme. By default, the `SemideterministicRounding` is used, but alternative scheme, `MagicRounding`, is also available." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_algorithms import VQE\n", + "from qiskit_algorithms.optimizers import COBYLA\n", + "from qiskit.circuit.library import RealAmplitudes\n", + "from qiskit.primitives import Estimator\n", + "\n", + "from qiskit_optimization.algorithms.qrao import (\n", + " QuantumRandomAccessOptimizer,\n", + " SemideterministicRounding,\n", + ")\n", + "\n", + "\n", + "# Prepare the VQE algorithm\n", + "ansatz = RealAmplitudes(2)\n", + "vqe = VQE(\n", + " ansatz=ansatz,\n", + " optimizer=COBYLA(),\n", + " estimator=Estimator(),\n", + ")\n", + "\n", + "# Use semi-deterministic rounding, known as \"Pauli rounding\"\n", + "# in https://arxiv.org/pdf/2111.03167v2.pdf\n", + "# (This is the default if no rounding scheme is specified.)\n", + "semidterministic_rounding = SemideterministicRounding()\n", + "\n", + "# Construct the optimizer\n", + "qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe, rounding_scheme=semidterministic_rounding)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we move forward with solving the problem by invoking the `solve()` method. It's important to note that when calling `solve()`, we pass the `problem` itself as an argument. Although we previously used `encode()` in `QuantumRandomAccessEncoding` to provide a clear understanding of the flow, `solve(problem)` automatically encodes the problem internally using `QuantumRandomAccessEncoding`. This provides a streamlined and simplified workflow that eliminates the need for explicit encoding steps.\n", + "\n", + "The result is provides us as a `QuantumRandomAccessOptimizationResult`.\n", + "The `x` contains the binary values representing the best solution found, while the `fval` contains the corresponding objective value.\n", + "\n", + "The `relaxed_fval` provides the expectation value of the relaxed Hamiltonian, adjusted to be in the units of the original optimization problem. For maximization problems, the best possible relaxed function value will always be greater than or equal to the best possible objective function value of the original problem. In practice, this often holds true for the best found value and best found objective function value as well." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The objective function value: 6.0\n", + "x: [1 0 1 1 0 1]\n", + "relaxed function value: 8.999999989772657\n", + "\n" + ] + } + ], + "source": [ + "# Solve the optimization problem\n", + "results = qrao.solve(problem)\n", + "\n", + "print(\n", + " f\"The objective function value: {results.fval}\\n\"\n", + " f\"x: {results.x}\\n\"\n", + " f\"relaxed function value: {-1 * results.relaxed_fval}\\n\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Interpret the solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the context of [max-cut](https://en.wikipedia.org/wiki/Maximum_cut), the result's \"optimal value\" tells us which subset each node belongs to given the partition found by the optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The obtained solution places a partition between nodes [1, 4] and nodes [0, 2, 3, 5].\n" + ] + }, + { + "data": { + "image/png": "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zpHd3ckXXyzmNQBYDEEJtN3CdycnkvwqyQNJ/pKkNu+M27cXvCwAFqlruRcA/P/p6VnI6GtJfpnkX0n+muTH8864e3ehrwFACTuUQNsNDCSdTX363EwynOpm5ieT/LSp409PJ2vWLHp5ACyQoATabt26pKPR7Sf/406qi2/+keR4qnMnm7d27fxjAGgN96EE2u6TT5Innphv1LYkf0y1Qzk6y/O/m3VWR0f1KzmffdZMtALQCoISWBZPPZWcP5/MzDQa8Ysk5+Y4wuwfXR0dyauvJlu3lq0PgOYJSmBZvPde8uyzrT1mZ2fy4IPVbYn6+lp7bAAacw4lsCyGh5PNm6vf3m6VmZnkzTfFJEC72aEEls233yZPPpl8/XV1ZXaJjo5ky5bk9ddbszYAmmeHElg2Dz2UvP9+8vDD5TuVzz2XvPZaS5YFwAIJSmBZPfZY9dvezzyz8Lm1WtLVlezbl7z9dmu/PgegeYISWHaPPJKcOpW89VayalX1WNccv+P1w3NPP518/HHy8svN3igdgKXgHErgnjIzk4yNJcePJ+Pj1f0kf7i1UE9Pdc7l0FDywgvJ448v71oBqAhK4J5WrydTU9XX2fffbycS4F4kKAEAKOJ/fQAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAighKAACKCEoAAIoISgAAinwHLd5f6WZQJB4AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "maxcut_partition = maxcut.interpret(results)\n", + "print(\n", + " f\"The obtained solution places a partition between nodes {maxcut_partition[0]} \"\n", + " f\"and nodes {maxcut_partition[1]}.\"\n", + ")\n", + "maxcut.draw(results, pos=nx.spring_layout(graph, seed=seed))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inspect the results of subroutines" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [MinimumEigensolverResult](https://qiskit.org/ecosystem/algorithms/stubs/qiskit_algorithms.MinimumEigensolverResult.html) that results from performing VQE on the relaxed Hamiltonian is available:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.relaxed_result" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The result of the rounding scheme is also worth considering. In this example, we used the `SemideterministricRounding`. It's important to note that with semi-deterministic rounding, a single sample is generated as the result, making it the optimal solution candidate.\n", + "\n", + "However, if we use the `MagicRounding` instead, multiple samples would be generated, each with a probability associated with it. These probabilities sum up to one, providing a distribution of potential optimal solutions." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[SolutionSample(x=array([1, 0, 1, 1, 0, 1]), fval=6.0, probability=1.0, status=)]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.samples" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exact Problem Solution with the `NumpyMinimumEigensolver`\n", + "\n", + "To assess the performance of QRAO in approximating the optimal solution, we can utilize the `NumpyMinimumEigensolver`, an exact classical optimizer. We can obtain the exact optimal solution to the problem as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "objective function value: 9.0\n", + "variable values: x_0=0.0, x_1=1.0, x_2=0.0, x_3=1.0, x_4=1.0, x_5=0.0\n", + "status: SUCCESS\n" + ] + } + ], + "source": [ + "from qiskit_algorithms import NumPyMinimumEigensolver\n", + "\n", + "from qiskit_optimization.algorithms import MinimumEigenOptimizer\n", + "\n", + "exact_mes = NumPyMinimumEigensolver()\n", + "exact = MinimumEigenOptimizer(exact_mes)\n", + "exact_result = exact.solve(problem)\n", + "print(exact_result.prettyprint())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The approximation ratio (QRAO's objective function value divided by the optimal objective function value) tells us how closely QRAO approximated the optimal solution to the problem." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QRAO Approximate Optimal Function Value: 6.0\n", + "Exact Optimal Function Value: 9.0\n", + "Approximation Ratio: 0.67\n" + ] + } + ], + "source": [ + "print(\"QRAO Approximate Optimal Function Value:\", results.fval)\n", + "print(\"Exact Optimal Function Value:\", exact_result.fval)\n", + "print(f\"Approximation Ratio: {results.fval / exact_result.fval :.2f}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solve the problem using the `QuantumRandomAccessOptimizer` with `MagicRounding`\n", + "\n", + "Magic rounding is a quantum technique employed to map the ground state results of our encoded Hamiltonian back to a solution of the original problem. Unlike semi-deterministic rounding, magic rounding requires a quantum backend, which can be either hardware or a simulator. \n", + "The backend is passed to the `MagicRounding` class through a `Sampler`, which also determines the total number of shots (samples) that magic rounding will utilize. Note that to specify the backend, you need to choose a `Sampler` from providers such as Aer or IBM Runtime. Consequently, we need to specify `Estimator` and `Sampler` for the optimizer and the rounding scheme, respectively.\n", + "\n", + "In practice, users may choose to set a significantly higher number of magic rounding shots compared to the shots used by the minimum eigensolver for the relaxed problem. This difference arises because the minimum eigensolver estimates expectation values, while the magic rounding scheme returns the sample corresponding to the maximum function value found. The number of magic rounding shots directly impacts the diversity of the computational basis we can generate.\n", + "When estimating an expectation value, increasing the number of shots enhances the convergence to the true value. However, when aiming to identify the largest possible function value, we often sample from the tail of a distribution of outcomes. As a result, until we observe the highest value outcome in our distribution, each additional shot increases the expected return value.\n", + "\n", + "In this tutorial, we use the `Estimator` for solving the relaxed Hamiltonian and the `Sampler` for performing magic rounding. Here, 10 times as many shots are used in the `Sampler`. As the number of qubits increases, you may need more shots or `weighted` basis sampling, as explained above.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import Sampler\n", + "\n", + "from qiskit_optimization.algorithms.qrao import MagicRounding\n", + "\n", + "\n", + "estimator = Estimator(options={\"shots\": 1000, \"seed\": seed})\n", + "sampler = Sampler(options={\"shots\": 10000, \"seed\": seed})\n", + "\n", + "# Prepare the VQE algorithm\n", + "ansatz = RealAmplitudes(2)\n", + "vqe = VQE(\n", + " ansatz=ansatz,\n", + " optimizer=COBYLA(),\n", + " estimator=estimator,\n", + ")\n", + "\n", + "\n", + "# Use magic rounding\n", + "magic_rounding = MagicRounding(sampler=sampler)\n", + "\n", + "# Construct the optimizer\n", + "qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe, rounding_scheme=magic_rounding)\n", + "\n", + "results = qrao.solve(problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The objective function value: 9.0\n", + "x: [1 0 1 0 0 1]\n", + "relaxed function value: 8.999996519407159\n", + "\n" + ] + } + ], + "source": [ + "print(\n", + " f\"The objective function value: {results.fval}\\n\"\n", + " f\"x: {results.x}\\n\"\n", + " f\"relaxed function value: {-1 * results.relaxed_fval}\\n\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since magic rounding relies on nondeterministic measurements, the method collects a number of samples based on the shots count provided to the `Sampler` mentioned earlier. These samples are then consolidated, taking into account duplicates and calculating the empirical probability for each `SolutionSample`. Each sample in the consolidation process includes a corresponding function value (`fval`).\n", + "\n", + "From the consolidated samples, we select the sample with the \"optimal\" function value. In the case of a max-cut problem, this means choosing the sample with the largest function value as our solution." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The number of distinct samples is 56.\n", + "Top 10 samples with the largest fval:\n", + "SolutionSample(x=array([1, 0, 1, 0, 0, 1]), fval=9.0, probability=0.0094, status=)\n", + "SolutionSample(x=array([0, 1, 0, 1, 1, 0]), fval=9.0, probability=0.0112, status=)\n", + "SolutionSample(x=array([0, 0, 0, 1, 1, 0]), fval=6.0, probability=0.0195, status=)\n", + "SolutionSample(x=array([1, 1, 1, 0, 0, 1]), fval=6.0, probability=0.0205, status=)\n", + "SolutionSample(x=array([0, 1, 1, 1, 1, 0]), fval=6.0, probability=0.0214, status=)\n", + "SolutionSample(x=array([1, 0, 0, 0, 0, 1]), fval=6.0, probability=0.0194, status=)\n", + "SolutionSample(x=array([1, 0, 1, 0, 0, 0]), fval=6.0, probability=0.0204, status=)\n", + "SolutionSample(x=array([0, 1, 0, 1, 1, 1]), fval=6.0, probability=0.021599999999999998, status=)\n", + "SolutionSample(x=array([1, 0, 1, 0, 1, 1]), fval=6.0, probability=0.02, status=)\n", + "SolutionSample(x=array([0, 1, 0, 1, 0, 0]), fval=6.0, probability=0.021, status=)\n" + ] + } + ], + "source": [ + "print(f\"The number of distinct samples is {len(results.samples)}.\")\n", + "print(\"Top 10 samples with the largest fval:\")\n", + "for sample in results.samples[:10]:\n", + " print(sample)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Alternative: Solve the Problem in Two Explicit Steps\n", + "\n", + "In the previous part of this tutorial, we utilized the `qrao.solve()` method, which solved the encoded problem (the ground state of the relaxed Hamiltonian) and performed rounding to map the ground state results back to a solution of the original problem. However, it is also possible to explicitly break down the calculation into these two distinct steps. This can be beneficial, especially when comparing solutions obtained across multiple rounding schemes applied to a candidate ground state.\n", + "\n", + "In this section, we will explore how to perform each of these steps explicitly.\n", + "\n", + "## Manually solve the relaxed problem.\n", + "\n", + "Let's start by invoking the `qrao.solve_relaxed()` method to directly solve the relaxed problem encoded by `QuantumRandomAccessEncoding`.\n", + "This method allows us to focus solely on solving the relaxed problem without performing rounding.\n", + "\n", + "By invoking `qrao.solve_relaxed()`, we obtain two essential outputs:\n", + "\n", + "- `MinimumEigensolverResult`: This object contains the results of running the minimum eigen optimizer such as the VQE on the relaxed problem. It provides information about the eigenvalue, and other relevant details. You can refer to the Qiskit Algorithms [documentation](https://qiskit.org/documentation/stubs/qiskit.algorithms.MinimumEigensolverResult.html) for a comprehensive explanation of the entries within this object.\n", + "- `RoundingContext`: This object encapsulates essential information about the encoding and the solution of the relaxed problem in a form that is ready for consumption by the rounding schemes." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# Encode the QUBO problem into a relaxed Hamiltonian\n", + "encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3)\n", + "encoding.encode(problem)\n", + "\n", + "# Solve the relaxed problem\n", + "relaxed_results, rounding_context = qrao.solve_relaxed(encoding)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "aux_operators_evaluated: [(0.010835872623325702, {'variance': 0.9999999914513272, 'shots': 1000}), (0.026074300411246972, {'variance': 0.999999991452347, 'shots': 1000}), (0.01044933784106082, {'variance': 1.0, 'shots': 1000}), (-0.04120945001189341, {'variance': 1.0, 'shots': 1000}), (0.02868127134978543, {'variance': 0.9999999973575187, 'shots': 1000}), (0.014064208211884945, {'variance': 0.9999999973585384, 'shots': 1000})]\n", + "combine: >\n", + "cost_function_evals: 114\n", + "eigenvalue: -4.499994593889271\n", + "optimal_circuit: ┌──────────────────────────────────────────────────────────┐\n", + "q_0: ┤0 ├\n", + " │ RealAmplitudes(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5],θ[6],θ[7]) │\n", + "q_1: ┤1 ├\n", + " └──────────────────────────────────────────────────────────┘\n", + "optimal_parameters: {ParameterVectorElement(θ[0]): 0.3782657558818425, ParameterVectorElement(θ[1]): 2.6307309944567154, ParameterVectorElement(θ[2]): -1.872906908815765, ParameterVectorElement(θ[3]): 0.1989998525444124, ParameterVectorElement(θ[4]): -2.8660234975739094, ParameterVectorElement(θ[5]): -0.9853046968649906, ParameterVectorElement(θ[6]): -0.7699284547923341, ParameterVectorElement(θ[7]): 3.5498132912316986}\n", + "optimal_point: [ 0.37826576 2.63073099 -1.87290691 0.19899985 -2.8660235 -0.9853047\n", + " -0.76992845 3.54981329]\n", + "optimal_value: -4.499994593889271\n", + "optimizer_evals: None\n", + "optimizer_result: { 'fun': -4.499994593889271,\n", + " 'jac': None,\n", + " 'nfev': 114,\n", + " 'nit': None,\n", + " 'njev': None,\n", + " 'x': array([ 0.37826576, 2.63073099, -1.87290691, 0.19899985, -2.8660235 ,\n", + " -0.9853047 , -0.76992845, 3.54981329])}\n", + "optimizer_time: 0.19381928443908691\n" + ] + } + ], + "source": [ + "for k in dir(relaxed_results):\n", + " if not k.startswith(\"_\"):\n", + " print(f\"{k}: {getattr(relaxed_results, k)}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Manually perform rounding on the relaxed problem results\n", + "\n", + "Next, we proceed with rounding the results obtained from solving the relaxed problem. To achieve this, we call the `round()` method on an instance of the desired rounding scheme and pass it the `RoundingContext` object. Below, we provide an example for both rounding schemes, utilizing the relaxed solution obtained in the previous step.\n", + "\n", + "By manually performing the rounding step, we have more flexibility and control over the rounding scheme applied to the relaxed problem results. This allows for greater exploration and comparison of different rounding strategies." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The objective function value: 3.0\n", + "x: [0 0 0 1 0 0]\n", + "relaxed function value: -8.999994593889271\n", + "The number of distinct samples is 1.\n" + ] + } + ], + "source": [ + "# Round the relaxed solution using semi-deterministic rounding\n", + "semidterministic_rounding = SemideterministicRounding()\n", + "sdr_results = semidterministic_rounding.round(rounding_context)\n", + "qrao_results_sdr = qrao.process_result(\n", + " problem=problem, encoding=encoding, relaxed_result=relaxed_results, rounding_result=sdr_results\n", + ")\n", + "\n", + "print(\n", + " f\"The objective function value: {qrao_results_sdr.fval}\\n\"\n", + " f\"x: {qrao_results_sdr.x}\\n\"\n", + " f\"relaxed function value: {-1 * qrao_results_sdr.relaxed_fval}\\n\"\n", + " f\"The number of distinct samples is {len(qrao_results_sdr.samples)}.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The objective function value: 9.0\n", + "x: [1 0 1 0 0 1]\n", + "relaxed function value: -8.999994593889271\n", + "The number of distinct samples is 56.\n" + ] + } + ], + "source": [ + "magic_rounding = MagicRounding(sampler=sampler)\n", + "mr_results = magic_rounding.round(rounding_context)\n", + "qrao_results_mr = qrao.process_result(\n", + " problem=problem, encoding=encoding, relaxed_result=relaxed_results, rounding_result=mr_results\n", + ")\n", + "\n", + "print(\n", + " f\"The objective function value: {qrao_results_mr.fval}\\n\"\n", + " f\"x: {qrao_results_mr.x}\\n\"\n", + " f\"relaxed function value: {-1 * qrao_results_mr.relaxed_fval}\\n\"\n", + " f\"The number of distinct samples is {len(qrao_results_mr.samples)}.\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Appendix\n", + "### How to verify correctness of your encoding\n", + "We assume for sake of the QRAO method that **the relaxation commutes with the objective function.** This notebook demonstrates how one can verify this for any problem (a `QuadraticProgram` in the language of Qiskit Optimization). One might want to verify this for pedagogical purposes, or as a sanity check when investigating unexpected behavior with the QRAO. Any problem that does not commute should be considered a bug, and if such a problem is discovered, we encourage that you submit it as [an issue on GitHub](https://github.com/qiskit-community/qiskit-optimization/issues).\n", + "\n", + "The `EncodingCommutationVerifier` class allows one to conveniently iterate over all decision variable states and compare each objective value with the corresponding encoded objective value, in order to identify any discrepancy." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Problem name: Max-cut\n", + "\n", + "Maximize\n", + " -2*x_0*x_1 - 2*x_0*x_3 - 2*x_0*x_4 - 2*x_1*x_2 - 2*x_1*x_5 - 2*x_2*x_3\n", + " - 2*x_2*x_4 - 2*x_3*x_5 - 2*x_4*x_5 + 3*x_0 + 3*x_1 + 3*x_2 + 3*x_3 + 3*x_4\n", + " + 3*x_5\n", + "\n", + "Subject to\n", + " No constraints\n", + "\n", + " Binary variables (6)\n", + " x_0 x_1 x_2 x_3 x_4 x_5\n", + "\n" + ] + }, + { + "data": { + "image/png": "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zxcAkBxBCSAn1i3z6k//up50+3j4c0nSveAHuhf8YNL2VEEZT9hav3z6t8cBAGYISgA58VRVsVb4ynXp88GH/kv5xLoDsxr3/ru8ee0iAGQPz1ACyJk2f51cKV6/OBsuh6U2f11axwQApEZQAsiZ+aWF2rtmcVrH3LdmseZPMeEHAJBeBCWAnPrlM1698nx1Wsb63fM1+sUzPDsJANlm2bY90zegAUDanb4cUuvbfYol7KQzvx/G6bDkcljat2YxMQkAOUJQApgzbg6NakegV+8Hb8vpsFKG5f3tK3zlavPXcZsbAHKIoAQw51wL39Vb3SF1Xh1QaHBU379IWfpm0fKG6kqtX+5lNjcAzAEEJYA5LRKN6fpgROOxhPJdDi0oK+INOAAwxxCUAAAAMMIsbwAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABghKAEAAGCEoAQAAIARghIAAABGCEoAAAAYISgBAABghKAEAACAEYISAAAARghKAAAAGCEoAQAAYISgBAAAgBGCEgAAAEYISgAAABj5D742zA/EXDkvAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit_optimization.algorithms.qrao import EncodingCommutationVerifier\n", + "\n", + "seed = 1\n", + "num_nodes = 6\n", + "graph = nx.random_regular_graph(d=3, n=num_nodes, seed=seed)\n", + "nx.draw(graph, with_labels=True, pos=nx.spring_layout(graph, seed=seed))\n", + "\n", + "maxcut = Maxcut(graph)\n", + "problem = maxcut.to_quadratic_program()\n", + "print(problem.prettyprint())" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As before, we `encode()` the problem using the QuantumRandomAccessEncoding class:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoded Problem:\n", + "=================\n", + "SparsePauliOp(['XX', 'XY', 'XZ', 'YX', 'ZX', 'YY', 'YZ', 'ZY', 'ZZ'],\n", + " coeffs=[1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j, 1.5+0.j,\n", + " 1.5+0.j])\n", + "Offset = -4.5\n", + "Variables encoded on each qubit: [[0, 2, 5], [1, 3, 4]]\n" + ] + } + ], + "source": [ + "encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3)\n", + "encoding.encode(problem)\n", + "\n", + "print(\"Encoded Problem:\\n=================\")\n", + "print(encoding.qubit_op) # The Hamiltonian without the offset\n", + "print(\"Offset = \", encoding.offset)\n", + "print(\"Variables encoded on each qubit: \", encoding.q2vars)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we iterate over every decision variable state using `EncodingCommutationVerifier` and verify that, in each case, the problem objective value matches the encoded objective value:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "verifier = EncodingCommutationVerifier(encoding, estimator=Estimator())\n", + "if not len(verifier) == 2**encoding.num_vars:\n", + " print(\"The number results of the encoded problem is not equal to 2 ** num_vars.\")\n", + "\n", + "for str_dvars, obj_val, encoded_obj_val in verifier:\n", + " if not np.isclose(obj_val, encoded_obj_val):\n", + " print(\n", + " f\"Violation identified: {str_dvars} evaluates to {obj_val} \"\n", + " f\"but the encoded problem evaluates to {encoded_obj_val}.\"\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you are able to construct a problem that causes a violation, it is quite possible that you have discovered a bug in the `QuantumRandomAccessEncoding` logic. We would greatly appreciate it if you could share the problem with us by [submitting it as an issue](https://github.com/Qiskit/qiskit-optimization/issues) on GitHub." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "

Version Information

Qiskit SoftwareVersion
qiskit-terra0.24.0.dev0+8a52d88
qiskit-aer0.12.0
qiskit-optimization0.6.0
System information
Python version3.9.10
Python compilerClang 13.1.6 (clang-1316.0.21.2.5)
Python buildmain, Aug 9 2022 18:26:17
OSDarwin
CPUs10
Memory (Gb)64.0
Thu Sep 07 21:53:47 2023 JST
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "

This code is a part of Qiskit

© Copyright IBM 2017, 2023.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import qiskit.tools.jupyter\n", + "\n", + "%qiskit_version_table\n", + "%qiskit_copyright" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "091032f753fd8b52e996fc03d6323ff523db19d64607db6acd8c3783811ef968" + }, + "kernelspec": { + "display_name": "Python 3.9.12 ('prototype-qrao')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/qiskit_optimization/algorithms/__init__.py b/qiskit_optimization/algorithms/__init__.py index c4ab6b0e8..515dd1518 100644 --- a/qiskit_optimization/algorithms/__init__.py +++ b/qiskit_optimization/algorithms/__init__.py @@ -62,6 +62,14 @@ WarmStartQAOAOptimizer WarmStartQAOAFactory +Submodules +========== + +.. autosummary:: + :toctree: + + qrao + """ from .admm_optimizer import ( diff --git a/qiskit_optimization/algorithms/optimization_algorithm.py b/qiskit_optimization/algorithms/optimization_algorithm.py index fb6cee1ad..62a63c18d 100644 --- a/qiskit_optimization/algorithms/optimization_algorithm.py +++ b/qiskit_optimization/algorithms/optimization_algorithm.py @@ -11,12 +11,13 @@ # that they have been altered from the originals. """An abstract class for optimization algorithms in Qiskit optimization module.""" +from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum from logging import getLogger -from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast +from typing import Any, Dict, List, Tuple, Type, Union, cast import numpy as np from qiskit.quantum_info import Statevector @@ -98,12 +99,12 @@ class OptimizationResult: def __init__( self, - x: Optional[Union[List[float], np.ndarray]], - fval: Optional[float], + x: Union[List[float], np.ndarray] | None, + fval: float | None, variables: List[Variable], status: OptimizationResultStatus, - raw_results: Optional[Any] = None, - samples: Optional[List[SolutionSample]] = None, + raw_results: Any | None = None, + samples: List[SolutionSample] | None = None, ) -> None: """ Args: @@ -215,7 +216,7 @@ def get_correlations(self) -> np.ndarray: return correlations @property - def x(self) -> Optional[np.ndarray]: + def x(self) -> np.ndarray | None: """Returns the variable values found in the optimization or None in case of FAILURE. Returns: @@ -224,7 +225,7 @@ def x(self) -> Optional[np.ndarray]: return self._x @property - def fval(self) -> Optional[float]: + def fval(self) -> float | None: """Returns the objective function value. Returns: @@ -374,8 +375,8 @@ def _get_feasibility_status( @staticmethod def _prepare_converters( - converters: Optional[Union[QuadraticProgramConverter, List[QuadraticProgramConverter]]], - penalty: Optional[float] = None, + converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None, + penalty: float | None = None, ) -> List[QuadraticProgramConverter]: """Prepare a list of converters from the input. @@ -430,7 +431,7 @@ def _convert( @staticmethod def _check_converters( - converters: Optional[Union[QuadraticProgramConverter, List[QuadraticProgramConverter]]] + converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None, ) -> List[QuadraticProgramConverter]: if converters is None: converters = [] @@ -445,9 +446,7 @@ def _interpret( cls, x: np.ndarray, problem: QuadraticProgram, - converters: Optional[ - Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] - ] = None, + converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None = None, result_class: Type[OptimizationResult] = OptimizationResult, **kwargs, ) -> OptimizationResult: @@ -490,7 +489,7 @@ def _interpret_samples( cls, problem: QuadraticProgram, raw_samples: List[SolutionSample], - converters: List[QuadraticProgramConverter], + converters: QuadraticProgramConverter | list[QuadraticProgramConverter] | None = None, ) -> Tuple[List[SolutionSample], SolutionSample]: """Interpret and sort all samples and return the raw sample corresponding to the best one""" converters = cls._check_converters(converters) diff --git a/qiskit_optimization/algorithms/qrao/__init__.py b/qiskit_optimization/algorithms/qrao/__init__.py new file mode 100644 index 000000000..0e54689e3 --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/__init__.py @@ -0,0 +1,141 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +r""" +Quantum Random Access Optimization (:mod:`qiskit_optimization.algorithms.qrao`) +=============================================================================== + +.. currentmodule:: qiskit_optimization.algorithms.qrao + +The Quantum Random Access Optimization (QRAO) module is designed to enable users to leverage a new +quantum method for combinatorial optimization problems [1]. This approach incorporates +Quantum Random Access Codes (QRACs) as a tool to encode multiple classical binary variables into a +single qubit, thereby saving quantum resources and enabling exploration of larger problem instances +on a quantum computer. The encoding produce a local quantum Hamiltonian whose ground state can be +approximated with standard algorithms such as VQE, and then rounded to yield approximation solutions +of the original problem. + +QRAO through a series of 3 classes: + +* The encoding class (:class:`~.QuantumRandomAccessEncoding`): This class encodes the original + problem into a relaxed problem that requires fewer resources to solve. +* The rounding schemes (:class:`~.SemideterministicRounding` and :class:`~.MagicRounding`): This + scheme is used to round the solution obtained from the relaxed problem back to a solution of + the original problem. +* The optimizer class (:class:`~.QuantumRandomAccessOptimizer`): This class performs the high-level + optimization algorithm, utilizing the capabilities of the encoding class and the rounding scheme. + +:class:`~.QuantumRandomAccessOptimizer` has two methods for solving problems, +:meth:`~.QuantumRandomAccessOptimizer.solve` and +:meth:`~.QuantumRandomAccessOptimizer.solve_relaxed`. The solve method provides a seamless +workflow by automatically managing the encoding and rounding procedures, as demonstrated in the +example below. This allows for a simplified and streamlined user experience. +On the other hand, the solve_relaxed method offers the flexibility to break the computation +process into distinct steps. This feature can be advantageous when we need to compare solutions +obtained from different rounding schemes applied to a potential ground state. + + +For example: + +.. code-block:: python + + from qiskit_algorithms.optimizers import COBYLA + from qiskit_algorithms import VQE + from qiskit.circuit.library import RealAmplitudes + from qiskit.primitives import Estimator + + from qiskit_optimization.algorithms.qrao import ( + QuantumRandomAccessOptimizer, + QuantumRandomAccessEncoding, + SemideterministicRounding, + ) + from qiskit_optimization.problems import QuadraticProgram + + problem = QuadraticProgram() + problem.binary_var("x") + problem.binary_var("y") + problem.binary_var("z") + problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + + ansatz = RealAmplitudes(1) + vqe = VQE( + ansatz=ansatz, + optimizer=COBYLA(), + estimator=Estimator(), + ) + # solve() automatically performs the encoding, optimization, and rounding + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + result = qrao.solve(problem) + + # solve_relaxed() only performs the optimization. The encoding and rounding must be done manually. + # encoding + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(problem) + # optimization + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + relaxed_results, rounding_context = qrao.solve_relaxed(encoding=encoding) + # rounding + rounding = SemideterministicRounding() + result = rounding.round(rounding_context) + + +[1] Bryce Fuller et al., Approximate Solutions of Combinatorial Problems via Quantum Relaxations, +`arXiv:2111.03167 `_ + + +Quantum Random Access Encoding and Optimization +=============================================== +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + EncodingCommutationVerifier + QuantumRandomAccessEncoding + QuantumRandomAccessOptimizer + QuantumRandomAccessOptimizationResult + +Rounding schemes +================ + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + MagicRounding + RoundingScheme + RoundingContext + RoundingResult + SemideterministicRounding + +""" + +from .encoding_commutation_verifier import EncodingCommutationVerifier +from .magic_rounding import MagicRounding +from .quantum_random_access_encoding import QuantumRandomAccessEncoding +from .quantum_random_access_optimizer import ( + QuantumRandomAccessOptimizationResult, + QuantumRandomAccessOptimizer, + SemideterministicRounding, +) +from .rounding_common import RoundingContext, RoundingResult, RoundingScheme + +__all__ = [ + "EncodingCommutationVerifier", + "QuantumRandomAccessEncoding", + "RoundingScheme", + "RoundingContext", + "RoundingResult", + "SemideterministicRounding", + "MagicRounding", + "QuantumRandomAccessOptimizer", + "QuantumRandomAccessOptimizationResult", +] diff --git a/qiskit_optimization/algorithms/qrao/encoding_commutation_verifier.py b/qiskit_optimization/algorithms/qrao/encoding_commutation_verifier.py new file mode 100644 index 000000000..315770fa0 --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/encoding_commutation_verifier.py @@ -0,0 +1,70 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The EncodingCommutationVerifier.""" + +from __future__ import annotations + +from qiskit.primitives import BaseEstimator + +from qiskit_optimization.exceptions import QiskitOptimizationError + +from .quantum_random_access_encoding import QuantumRandomAccessEncoding + + +class EncodingCommutationVerifier: + """Class for verifying that the relaxation commutes with the objective function.""" + + def __init__(self, encoding: QuantumRandomAccessEncoding, estimator: BaseEstimator): + """ + Args: + encoding: The encoding to verify. + estimator: The estimator to use for the verification. + """ + self._encoding = encoding + self._estimator = estimator + + def __len__(self) -> int: + return 2**self._encoding.num_vars + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __getitem__(self, i: int) -> tuple[str, float, float]: + if i < 0 or i >= len(self): + raise IndexError(f"Index out of range: {i}") + + encoding = self._encoding + str_dvars = f"{i:0{encoding.num_vars}b}" + dvars = [int(b) for b in str_dvars] + encoded_bitstr_qc = encoding.state_preparation_circuit(dvars) + + # Evaluate the original objective function + problem = encoding.problem + sense = problem.objective.sense.value + obj_val = problem.objective.evaluate(dvars) * sense + + # Evaluate the encoded Hamiltonian + encoded_op = encoding.qubit_op + offset = encoding.offset + + job = self._estimator.run([encoded_bitstr_qc], [encoded_op]) + + try: + encoded_obj_val = job.result().values[0] + offset + except Exception as exc: + raise QiskitOptimizationError( + "The primitive job to verify commutation failed!" + ) from exc + + return (str_dvars, obj_val, encoded_obj_val) diff --git a/qiskit_optimization/algorithms/qrao/magic_rounding.py b/qiskit_optimization/algorithms/qrao/magic_rounding.py new file mode 100644 index 000000000..02261d16b --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/magic_rounding.py @@ -0,0 +1,473 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Magic basis rounding module""" +from __future__ import annotations + +from collections import defaultdict + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.quantum_info import SparsePauliOp +from qiskit_algorithms.exceptions import AlgorithmError + +from qiskit_optimization.algorithms import OptimizationResultStatus, SolutionSample +from qiskit_optimization.exceptions import QiskitOptimizationError + +from .quantum_random_access_encoding import ( + _z_to_21p_qrac_basis_circuit, + _z_to_31p_qrac_basis_circuit, +) +from .rounding_common import RoundingContext, RoundingResult, RoundingScheme + + +class MagicRounding(RoundingScheme): + """Magic rounding scheme that measures in magic bases, and then uses the measurement results + to round the solution. Since the magic rounding is based on the measurement results, it + requires a quantum backend, which can be either hardware or a simulator. + + The details are described in https://arxiv.org/abs/2111.03167. + """ + + _DECODING = { + 3: ( # Eq. (8) + {"0": [0, 0, 0], "1": [1, 1, 1]}, # I mu+ I, I mu- I + {"0": [0, 1, 1], "1": [1, 0, 0]}, # X mu+ X, X mu- X + {"0": [1, 0, 1], "1": [0, 1, 0]}, # Y mu+ Y, Y mu- Y + {"0": [1, 1, 0], "1": [0, 0, 1]}, # Z mu+ Z, Z mu- Z + ), + 2: ( # Sec. VII + {"0": [0, 0], "1": [1, 1]}, # I xi+ I, I xi- I + {"0": [0, 1], "1": [1, 0]}, # X xi+ X, X xi- X + ), + 1: ({"0": [0], "1": [1]},), + } + + # Pauli op string to label index in ops + _OP_INDICES = {1: {"Z": 0}, 2: {"X": 0, "Z": 1}, 3: {"X": 0, "Y": 1, "Z": 2}} + + def __init__( + self, + sampler: BaseSampler, + basis_sampling: str = "uniform", + seed: int | None = None, + ): + """ + Args: + sampler: Sampler to use for sampling the magic bases. + basis_sampling: Method to use for sampling the magic bases. Must + be either ``"uniform"`` (default) or ``"weighted"``. + ``"uniform"`` samples all magic bases uniformly, and is the + method described in https://arxiv.org/abs/2111.03167. + ``"weighted"`` attempts to choose bases strategically using the + Pauli expectation values from the minimum eigensolver. + However, the approximation bounds given in + https://arxiv.org/abs/2111.03167 apply only to ``"uniform"`` + sampling. + seed: Seed for random number generator, which is used to sample the + magic bases. + + Raises: + ValueError: If ``basis_sampling`` is not ``"uniform"`` or ``"weighted"``. + ValueError: If the sampler is not configured with a number of shots. + """ + if basis_sampling not in ("uniform", "weighted"): + raise ValueError( + f"'{basis_sampling}' is not an implemented sampling method. " + "Please choose either 'uniform' or 'weighted'." + ) + self._sampler = sampler + self._rng = np.random.default_rng(seed) + self._basis_sampling = basis_sampling + if self._sampler.options.get("shots") is None: + raise ValueError("Magic rounding requires a sampler configured with a number of shots.") + self._shots = sampler.options.shots + super().__init__() + + @property + def sampler(self) -> BaseSampler: + """Returns the Sampler used to sample the magic bases.""" + return self._sampler + + @property + def basis_sampling(self): + """Basis sampling method (either ``"uniform"`` or ``"weighted"``).""" + return self._basis_sampling + + @staticmethod + def _make_circuits( + circuit: QuantumCircuit, bases: np.ndarray, vars_per_qubit: int + ) -> list[QuantumCircuit]: + """Make a list of circuits to measure in the given magic bases. + + Args: + circuit: Quantum circuit to measure. + bases: List of magic bases to measure in. + vars_per_qubit: Number of variables per qubit. + + Returns: + List of quantum circuits to measure in the given magic bases. + + Raises: + ValueError: If ``vars_per_qubit`` is not 1, 2, or 3. + """ + if vars_per_qubit not in (1, 2, 3): + raise ValueError("vars_per_qubit must be 1, 2, or 3.") + + circuits = [] + for basis in bases: + if vars_per_qubit == 3: + qc = circuit.compose(_z_to_31p_qrac_basis_circuit(basis).inverse(), inplace=False) + elif vars_per_qubit == 2: + qc = circuit.compose(_z_to_21p_qrac_basis_circuit(basis).inverse(), inplace=False) + elif vars_per_qubit == 1: + qc = circuit.copy() + qc.measure_all() + circuits.append(qc) + return circuits + + def _evaluate_magic_bases( + self, + circuit: QuantumCircuit, + bases: np.ndarray, + basis_shots: np.ndarray, + vars_per_qubit: int, + ) -> list[dict[str, int]]: + """ + Given a quantum circuit to measure, a list of magic bases to measure, and a list of the + shots to use for each magic basis configuration, measure the provided circuit in the magic + bases given and return the counts dictionaries associated with each basis measurement. + + Args: + circuit: The quantum circuit to measure. + bases: A list of magic bases to measure. + basis_shots: A list of shots to use for each magic basis configuration. + vars_per_qubit: The number of decision variables per qubit. + + Returns: + A list of counts dictionaries associated with each basis measurement. + + Raises: + AlgorithmError: If the primitive job failed. + QiskitOptimizationError: If the number of circuits and the number of basis types are + not the same. + QiskitOptimizationError: If the number of circuits and the results from the primitive + job are not the same. + QiskitOptimizationError: If some of the results from the primitive job are not collected. + """ + circuits = self._make_circuits(circuit, bases, vars_per_qubit) + # Execute each of the rotated circuits and collect the results + # Batch the circuits into jobs where each group has the same number of + # shots, so that you can wait for the queue as few times as possible if + # using hardware. + circuit_indices_by_shots: dict[int, list[int]] = defaultdict(list) + basis_counts: list[dict[str, int] | None] = [None] * len(circuits) + if len(circuits) != len(basis_shots): + raise QiskitOptimizationError( + "Internal error: The number of circuits and the number of basis types must be the same, " + f"{len(circuits)} != {len(basis_shots)}." + ) + + for i, shots in enumerate(basis_shots): + circuit_indices_by_shots[shots].append(i) + + for shots, indices in sorted(circuit_indices_by_shots.items(), reverse=True): + try: + job = self._sampler.run([circuits[i] for i in indices], shots=shots) + result = job.result() + except Exception as exc: + raise AlgorithmError( + "The primitive job to evaluate the magic state failed." + ) from exc + counts_list = [dist.binary_probabilities() for dist in result.quasi_dists] + if len(counts_list) != len(indices): + raise QiskitOptimizationError( + "Internal error: The number of circuits and the results from the primitive job " + f"must be the same, {len(indices)} != {len(counts_list)}." + ) + for i, counts in zip(indices, counts_list): + basis_counts[i] = counts + + if None in basis_counts: + raise QiskitOptimizationError( + "Internal error: Some basis counts were not collected. Please check the primitive job." + ) + + basis_counts = [ + {key: val * basis_shots[i] for key, val in counts.items()} + for i, counts in enumerate(basis_counts) + ] + + return basis_counts + + def _unpack_measurement_outcome( + self, + bits: str, + basis: list[int], + var2op: dict[int, tuple[int, SparsePauliOp]], + vars_per_qubit: int, + ) -> list[int]: + """ + Given a measurement outcome, a magic basis, and a mapping from decision variables to + Pauli operators, return the values of the decision variables. + + Args: + bits: The measurement outcome. + basis: The magic basis used for the measurement. + var2op: A mapping from decision variables to Pauli operators. + vars_per_qubit: The number of decision variables per qubit. + + Returns: + The values of the decision variables. + """ + output_bits = [] + # iterate in order over decision variables + for q, op in var2op.values(): + # get the decoding outcome index for the variable + # corresponding to this Pauli op. + op_index = self._OP_INDICES[vars_per_qubit][str(op.paulis[0])] + # get the bits associated to this magic basis' + # measurement outcomes + bit_outcomes = self._DECODING[vars_per_qubit][basis[q]] + # select which measurement outcome we observed + # this gives up to 3 bits of information + magic_bits = bit_outcomes[bits[q]] + # Assign our variable's value depending on + # which pauli our variable was associated to + variable_value = magic_bits[op_index] + output_bits.append(variable_value) + return output_bits + + def _compute_dv_counts( + self, + basis_counts: list[dict[str, int]], + bases: np.ndarray, + var2op: dict[int, tuple[int, SparsePauliOp]], + vars_per_qubit: int, + ): + """ + Given a list of bases, basis_shots, and basis_counts, convert + each observed bitstrings to its corresponding decision variable + configuration. Return the counts of each decision variable configuration. + + Args: + basis_counts: A list of counts dictionaries associated with each basis measurement. + bases: A list of magic bases to measure. + var2op: A mapping from decision variables to Pauli operators. + vars_per_qubit: The number of decision variables per qubit. + + Returns: + A dictionary of counts for each decision variable configuration. + """ + dv_counts: dict[str, int] = defaultdict(int) + for base, counts in zip(bases, basis_counts): + # For each measurement outcome... + for bitstr, count in counts.items(): + # For each bit in the observed bit string... + soln = self._unpack_measurement_outcome(bitstr, base, var2op, vars_per_qubit) + soln_str = "".join([str(bit) for bit in soln]) + dv_counts[soln_str] += count + return dv_counts + + def _sample_bases_uniform( + self, q2vars: list[list[int]], vars_per_qubit: int + ) -> tuple[np.ndarray, np.ndarray]: + """ + Sample measurement bases for each qubit uniformly at random. + + Args: + q2vars: A list of lists of integers. Each inner list contains the indices of decision + variables mapped to a specific qubit. + vars_per_qubit: The maximum number of decision variables that can be mapped to a + single qubit.. + + Returns: + A tuple containing two arrays: + bases: A 2D numpy array of shape (num_bases, num_qubits), where each row + corresponds to a basis configuration. Each element of the array is an + integer in the range [0, 2 ** (vars_per_qubit - 1) - 1]. The integer + represents the index of the basis to measure in for the corresponding + qubit. + basis_shots: A 1D numpy array of shape (num_bases,), where each element + corresponds to the number of shots to use for the corresponding basis in + the bases array. + """ + bases_ = self._rng.choice(2 ** (vars_per_qubit - 1), size=(self._shots, len(q2vars))) + bases, basis_shots = np.unique(bases_, axis=0, return_counts=True) + return bases, basis_shots + + def _sample_bases_weighted( + self, q2vars: list[list[int]], expectation_values: list[complex] | None, vars_per_qubit: int + ) -> tuple[np.ndarray, np.ndarray]: + """ + Perform weighted sampling from the expectation values. The goal is to make smarter choices + about which bases to measure in using the expectation values. + + Args: + q2vars: A list of lists of integers. Each inner list contains the indices of decision + variables mapped to a specific qubit. + expectation_values: A list of expectation values for each decision variable. + vars_per_qubit: The maximum number of decision variables that can be mapped to a + single qubit. + + Returns: + A tuple containing two arrays: + bases: A 2D numpy array of shape (num_bases, num_qubits), where each row + corresponds to a basis configuration. Each element of the array is an + integer in the range [0, 2 ** (vars_per_qubit - 1) - 1]. The integer + represents the index of the basis to measure in for the corresponding + qubit. + basis_shots: A 1D numpy array of shape (num_bases,), where each element + corresponds to the number of shots to use for the corresponding basis in + the bases array. + """ + # First, we make sure all Pauli expectation values have absolute value + # at most 1. Otherwise, some of the probabilities computed below might + # be negative. + clipped_expectation_values = np.clip(expectation_values, -1, 1) + # basis_probs will have num_qubits number of elements. + # Each element will be a list of length 4 specifying the + # probability of picking the corresponding magic basis on that qubit. + basis_probs = [] + for dvars in q2vars: + if vars_per_qubit == 3: + x = 0.5 * (1 - clipped_expectation_values[dvars[0]]) + y = 0.5 * (1 - clipped_expectation_values[dvars[1]]) if (len(dvars) > 1) else 0 + z = 0.5 * (1 - clipped_expectation_values[dvars[2]]) if (len(dvars) > 2) else 0 + # In the coefficient of the Pauli operator within the magic bases, 'p' represents a + # positive sign, while 'm' signifies a negative sign. + # The four combinations of these signs are used to define the quantum system behavior + # in the context of magic bases. + # ppp: mu± = .5(I ± 1/sqrt(3)( X + Y + Z)) + # pmm: X mu± X = .5(I ± 1/sqrt(3)( X - Y - Z)) + # mpm: Y mu± Y = .5(I ± 1/sqrt(3)(-X + Y - Z)) + # mmp: Z mu± Z = .5(I ± 1/sqrt(3)(-X - Y + Z)) + # fmt: off + ppp_mmm = x * y * z + (1-x) * (1-y) * (1-z) + pmm_mpp = x * (1-y) * (1-z) + (1-x) * y * z + mpm_pmp = (1-x) * y * (1-z) + x * (1-y) * z + ppm_mmp = x * y * (1-z) + (1-x) * (1-y) * z + # fmt: on + basis_probs.append([ppp_mmm, pmm_mpp, mpm_pmp, ppm_mmp]) + elif vars_per_qubit == 2: + x = 0.5 * (1 - clipped_expectation_values[dvars[0]]) + z = 0.5 * (1 - clipped_expectation_values[dvars[1]]) if (len(dvars) > 1) else 0 + # In the coefficient of the Pauli operator within the magic bases, 'p' represents a + # positive sign, while 'm' signifies a negative sign. + # The two combinations of these signs are used to define the quantum system behavior + # in the context of magic bases. + # pp: xi± = .5(I ± 1/sqrt(2)( X + Z )) + # pm: X xi± X = .5(I ± 1/sqrt(2)( X - Z )) + # fmt: off + pp_mm = x * z + (1-x) * (1-z) + pm_mp = x * (1-z) + (1-x) * z + # fmt: on + basis_probs.append([pp_mm, pm_mp]) + elif vars_per_qubit == 1: + basis_probs.append([1.0]) + bases_ = np.array( + [ + self._rng.choice( + 2 ** (vars_per_qubit - 1), p=[p.real for p in probs], size=self._shots + ) + for probs in basis_probs + ] + ).T + bases, basis_shots = np.unique(bases_, axis=0, return_counts=True) + return bases, basis_shots + + def round(self, rounding_context: RoundingContext) -> RoundingResult: + """Perform magic rounding using the given RoundingContext. + + Args: + rounding_context: The context containing the information needed for the rounding. + + Returns: + RoundingResult: The results of the magic rounding process. + + Raises: + ValueError: If the rounding context has no circuits. + ValueError: If the rounding context has no expectation values for magic rounding with the + weighted sampling. + QiskitOptimizationError: If the magic rounding did not return the expected number of shots. + QiskitOptimizationError: If the magic rounding did not return the expected number of bases. + """ + expectation_values = rounding_context.expectation_values + circuit = rounding_context.circuit + q2vars = rounding_context.encoding.q2vars + var2op = rounding_context.encoding.var2op + vars_per_qubit = rounding_context.encoding.max_vars_per_qubit + + if circuit is None: + raise ValueError( + "No circuit was provided in the rounding context. " + "Magic rounding requires a circuit to be available. " + "Perhaps try Semi-deterministic rounding instead." + ) + + if self.basis_sampling == "uniform": + # uniform sampling + bases, basis_shots = self._sample_bases_uniform(q2vars, vars_per_qubit) + else: + # weighted sampling + if expectation_values is None: + raise ValueError( + "No expectation values were provided in the rounding context. " + "Magic rounding with weighted sampling requires the expectation values of the " + "``RoundingContext`` to be available, but they are not. " + 'Try `basis_sampling="uniform"` instead.' + ) + bases, basis_shots = self._sample_bases_weighted( + q2vars, expectation_values, vars_per_qubit + ) + + # For each of the Magic Bases sampled above, measure + # the appropriate number of times (given by basis_shots) + # and return the circuit results + basis_counts = self._evaluate_magic_bases(circuit, bases, basis_shots, vars_per_qubit) + # keys will be configurations of decision variables + # values will be total number of observations. + soln_counts = self._compute_dv_counts(basis_counts, bases, var2op, vars_per_qubit) + + soln_samples = [ + SolutionSample( + x=np.asarray([int(bit) for bit in soln]), + fval=rounding_context.encoding.problem.objective.evaluate( + [int(bit) for bit in soln] + ), + probability=count / self._shots, + status=OptimizationResultStatus.SUCCESS + if rounding_context.encoding.problem.is_feasible([int(bit) for bit in soln]) + else OptimizationResultStatus.INFEASIBLE, + ) + for soln, count in soln_counts.items() + ] + if sum(soln_counts.values()) != self._shots: + raise QiskitOptimizationError( + f"Internal error: Magic rounding did not return the expected number of shots. " + f"Expected {self._shots}, got {sum(soln_counts.values())}." + ) + if not len(bases) == len(basis_shots) == len(basis_counts): + raise QiskitOptimizationError( + f"Internal error: sizes of bases({len(bases)}), basis_shots({len(basis_shots)}), " + f"and basis_counts({len(basis_counts)}) are not equal." + ) + + # Create a MagicRoundingResult object to return + return RoundingResult( + expectation_values=expectation_values, + samples=soln_samples, + bases=bases, + basis_shots=basis_shots, + basis_counts=basis_counts, + ) diff --git a/qiskit_optimization/algorithms/qrao/quantum_random_access_encoding.py b/qiskit_optimization/algorithms/qrao/quantum_random_access_encoding.py new file mode 100644 index 000000000..c438134f2 --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/quantum_random_access_encoding.py @@ -0,0 +1,578 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Quantum Random Access Encoding module.""" +from __future__ import annotations + +from collections import defaultdict +from functools import reduce +from typing import cast + +import networkx as nx +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import SparsePauliOp + +from qiskit_optimization.exceptions import QiskitOptimizationError +from qiskit_optimization.problems.quadratic_program import QuadraticProgram + + +def _z_to_31p_qrac_basis_circuit(bases: list[int], bit_flip: int = 0) -> QuantumCircuit: + """Return the circuit that implements the rotation to the (3,1,p)-QRAC. + + Args: + bases: The basis, 0, 1, 2, or 3, for the qubit. + bit_flip: Whether to flip the state of the qubit. 1 for flip, 0 for no flip. + + Returns: + The ``QuantumCircuit`` implementing the rotation to the (3,1,p)-QRAC. + + Raises: + ValueError: If the basis is not 0, 1, 2, or 3 + """ + circ = QuantumCircuit(len(bases)) + BETA = np.arccos(1 / np.sqrt(3)) # pylint: disable=invalid-name + + for i, base in enumerate(reversed(bases)): + if bit_flip: + # if bit_flip == 1: then flip the state of the qubit to |1> + circ.x(i) + + if base == 0: + circ.r(-BETA, -np.pi / 4, i) + elif base == 1: + circ.r(np.pi - BETA, np.pi / 4, i) + elif base == 2: + circ.r(np.pi + BETA, np.pi / 4, i) + elif base == 3: + circ.r(BETA, -np.pi / 4, i) + else: + raise ValueError(f"Unknown basis: {base}. Basis must be 0, 1, 2, or 3.") + return circ + + +def _z_to_21p_qrac_basis_circuit(bases: list[int], bit_flip: int = 0) -> QuantumCircuit: + """Return the circuit that implements the rotation to the (2,1,p)-QRAC. + + Args: + bases: The basis, 0, 1, for the qubit. + bit_flip: Whether to flip the state of the qubit. 1 for flip, 0 for no flip. + + Returns: + The ``QuantumCircuit`` implementing the rotation to the (2,1,p)-QRAC. + + Raises: + ValueError: if the basis is not 0 or 1 + """ + circ = QuantumCircuit(len(bases)) + + for i, base in enumerate(reversed(bases)): + if bit_flip: + # if bit_flip == 1: then flip the state of the qubit to |1> + circ.x(i) + + if base == 0: + circ.r(-1 * np.pi / 4, -np.pi / 2, i) + elif base == 1: + circ.r(-3 * np.pi / 4, -np.pi / 2, i) + else: + raise ValueError(f"Unknown basis: {base}. Basis must be 0, 1.") + return circ + + +def _qrac_state_prep_1q(bit_list: list[int]) -> QuantumCircuit: + """ + Return the circuit that prepares the state for a (1,1,p), (2,1,p), or (3,1,p)-QRAC. + + Args: + bit_list: The bitstring to prepare. If 1 argument is given, then a (1,1,p)-QRAC is generated. + If 2 arguments are given, then a (2,1,p)-QRAC is generated. If 3 arguments are given, + then a (3,1,p)-QRAC is generated. + + Returns: + The ``QuantumCircuit`` implementing the state preparation. + + Raises: + TypeError: if the number of arguments is not 1, 2, or 3 + ValueError: if any of the arguments are not 0 or 1 + """ + # pylint: disable=C0401 + if len(bit_list) not in (1, 2, 3): + raise TypeError(f"qrac_state_prep_1q requires 1, 2, or 3 arguments, not {len(bit_list)}.") + if not all(bit in (0, 1) for bit in bit_list): + raise ValueError("Each argument to qrac_state_prep_1q must be 0 or 1.") + + if len(bit_list) == 3: + # Prepare (3,1,p)-qrac + # In the following lines, the input bits are XORed to match the + # conventions used in the paper. + # To understand why this transformation happens, + # observe that the two states that define each magic basis + # correspond to the same bitstrings but with a global bit flip. + # Thus the three bits of information we use to construct these states are: + # base_index0,base_index1 : two bits to pick one of four magic bases + # bit_flip: one bit to indicate which magic basis projector we are interested in. + + bit_flip = bit_list[0] ^ bit_list[1] ^ bit_list[2] + base_index0 = bit_list[1] ^ bit_list[2] + base_index1 = bit_list[0] ^ bit_list[2] + + # This is a convention chosen to be consistent with https://arxiv.org/abs/2111.03167 + # See SI:4 second paragraph and observe that π+ = |0X0|, π- = |1X1| + base = [2 * base_index0 + base_index1] + circ = _z_to_31p_qrac_basis_circuit(base, bit_flip) + + elif len(bit_list) == 2: + # Prepare (2,1,p)-qrac + # (00,01) or (10,11) + bit_flip = bit_list[0] + # (00,11) or (01,10) + base_index0 = bit_list[0] ^ bit_list[1] + + # This is a convention chosen to be consistent with https://arxiv.org/abs/2111.03167 + # # See SI:4 second paragraph and observe that π+ = |0X0|, π- = |1X1| + base = [base_index0] + circ = _z_to_21p_qrac_basis_circuit(base, bit_flip) + + else: + bit_flip = bit_list[0] + circ = QuantumCircuit(1) + if bit_flip: + circ.x(0) + + return circ + + +def _qrac_state_prep_multi_qubit( + x: list[int], + q2vars: list[list[int]], + max_vars_per_qubit: int, +) -> QuantumCircuit: + """Prepares a multi qubit QRAC state. + + Args: + x: The state of each decision variable (0 or 1). + q2vars: A list of lists of integers. Each inner list contains the indices of decision variables + mapped to a specific qubit. + max_vars_per_qubit: The maximum number of decision variables that can be mapped to a + single qubit. + + Returns: + A QuantumCircuit object representing the prepared state. + + Raises: + ValueError: If any qubit is associated with more than `max_vars_per_qubit` variables. + ValueError: If a decision variable in ``q2vars`` is not included in `x`. + ValueError: If there are unused decision variables in `x` after mapping to qubits. + """ + # Create a set of all remaining decision variables + remaining_dvars = set(range(len(x))) + # Create a list to store the binary mappings of each qubit to its corresponding decision variables + variable_mappings: list[list[int]] = [] + # Check that each qubit is associated with at most max_vars_per_qubit variables + for qi_vars in q2vars: + if len(qi_vars) > max_vars_per_qubit: + raise ValueError( + "Each qubit is expected to be associated with at most " + f"`max_vars_per_qubit` ({max_vars_per_qubit}) variables, " + f"not {len(qi_vars)} variables." + ) + # Create a list to store the binary mapping of the current qubit + qi_bits: list[int] = [] + + # Map each decision variable associated with the current qubit to a binary value and add it + # to the qubit bits + for dvar in qi_vars: + try: + qi_bits.append(x[dvar]) + except IndexError: + raise ValueError(f"Decision variable not included in dvars: {dvar}") from None + try: + remaining_dvars.remove(dvar) + except KeyError: + raise ValueError( + f"Unused decision variable(s) in dvars: {remaining_dvars}" + ) from None + + # Pad with zeros if necessary + while len(qi_bits) < max_vars_per_qubit: + qi_bits.append(0) + + variable_mappings.append(qi_bits) + + # Raise an error if not all decision variables are used + if remaining_dvars: + raise ValueError(f"Not all dvars were included in q2vars: {remaining_dvars}") + + # Prepare the individual qrac circuit and combine them into a multi qubit circuit + qracs = [_qrac_state_prep_1q(qi_bits) for qi_bits in variable_mappings] + qrac_circ = reduce(lambda x, y: x.tensor(y), qracs) + return qrac_circ + + +class QuantumRandomAccessEncoding: + """This class specifies a Quantum Random Access Code that can be used to encode + the binary variables of a QUBO (quadratic unconstrained binary optimization + problem). + + """ + + # This defines the convention of the Pauli operators (and their ordering) + # for each encoding. + _OPERATORS = ( + (SparsePauliOp("Z"),), # (1,1,1) QRAC + (SparsePauliOp("X"), SparsePauliOp("Z")), # (2,1,p) QRAC, p ≈ 0.85 + (SparsePauliOp("X"), SparsePauliOp("Y"), SparsePauliOp("Z")), # (3,1,p) QRAC, p ≈ 0.79 + ) + + def __init__(self, max_vars_per_qubit: int = 3): + """ + Args: + max_vars_per_qubit: The maximum number of decision variables per qubit. + Integer values 1, 2 and 3 are supported (default to 3). + """ + if max_vars_per_qubit not in (1, 2, 3): + raise ValueError("max_vars_per_qubit must be 1, 2, or 3") + self._ops = self._OPERATORS[max_vars_per_qubit - 1] + + self._qubit_op: SparsePauliOp | None = None + self._offset: float | None = None + self._problem: QuadraticProgram | None = None + self._var2op: dict[int, tuple[int, SparsePauliOp]] = {} + self._q2vars: list[list[int]] = [] + self._frozen = False + + @property + def num_qubits(self) -> int: + """Number of qubits""" + return len(self._q2vars) + + @property + def num_vars(self) -> int: + """Number of decision variables""" + return len(self._var2op) + + @property + def max_vars_per_qubit(self) -> int: + """Maximum number of variables per qubit""" + return len(self._ops) + + @property + def var2op(self) -> dict[int, tuple[int, SparsePauliOp]]: + """Maps each decision variable to ``(qubit_index, operator)``""" + return self._var2op + + @property + def q2vars(self) -> list[list[int]]: + """Each element contains the list of decision variable indices encoded on that qubit""" + return self._q2vars + + @property + def compression_ratio(self) -> float: + """Compression ratio. Number of decision variables divided by number of qubits""" + return self.num_vars / self.num_qubits + + @property + def minimum_recovery_probability(self) -> float: + """Minimum recovery probability, as set by ``max_vars_per_qubit``""" + n = self.max_vars_per_qubit + return (1 + 1 / np.sqrt(n)) / 2 + + @property + def qubit_op(self) -> SparsePauliOp: + """Relaxed Hamiltonian operator. + + Raises: + RuntimeError: If the objective function has not been set yet. Use the ``encode`` method + to construct the Hamiltonian, or make sure that the objective function has been set. + """ + if self._qubit_op is None: + raise RuntimeError( + "Cannot return the relaxed Hamiltonian operator: no objective function has been " + "provided yet. Use the ``encode`` method to construct the Hamiltonian, or make " + "sure that the objective function has been set." + ) + return self._qubit_op + + @property + def offset(self) -> float: + """Relaxed Hamiltonian offset + + Raises: + RuntimeError: If the offset has not been set yet. Use the ``encode`` method to construct + the Hamiltonian, or make sure that the objective function has been set. + """ + if self._offset is None: + raise RuntimeError( + "Cannot return the relaxed Hamiltonian offset: The offset attribute cannot be " + "accessed until the ``encode`` method has been called to generate the qubit " + "Hamiltonian. Please call ``encode`` first." + ) + return self._offset + + @property + def problem(self) -> QuadraticProgram: + """The ``QuadraticProgram`` encoding a QUBO optimization problem + + Raises: + RuntimeError: If the ``QuadraticProgram`` has not been set yet. Use the ``encode`` + method to set the problem. + """ + if self._problem is None: + raise RuntimeError( + "This object has not been associated with a ``QuadraticProgram``. " + "Please use the ``encode`` method to set the problem." + ) + return self._problem + + def freeze(self): + """Freeze the object to prevent further modification. + + Once an instance of this class is frozen, ``encode`` can no longer be called. + """ + if not self._frozen: + self._qubit_op = self._qubit_op.simplify(atol=0) + self._frozen = True + + @property + def frozen(self) -> bool: + """Whether the object is frozen or not.""" + return self._frozen + + def _add_variables(self, variables: list[int]) -> None: + """Add variables to the Encoding object. + + Args: + variables: A list of variable indices to be added. + + Raises: + ValueError: If added variables are not unique. + ValueError: If added variables collide with existing ones. + + """ + # NOTE: If this is called multiple times, it *always* adds an + # additional qubit (see final line), even if aggregating them into a + # single call would have resulted in fewer qubits. + + # Check if variables is empty + if not variables: + return + + # Check if variables are unique + if len(variables) != len(set(variables)): + raise ValueError("Added variables must be unique") + + # Check if variables collide with existing ones + for v in variables: + if v in self._var2op: + raise ValueError("Added variables cannot collide with existing ones") + + # Calculate the number of new qubits required for the added variables. + n = len(self._ops) + old_num_qubits = len(self._q2vars) + num_new_qubits = int(np.ceil(len(variables) / n)) + # Add the new qubits to _q2vars. + for _ in range(num_new_qubits): + self._q2vars.append([]) + # Associate each added variable with a qubit and operator. + for i, v in enumerate(variables): + qubit, op = divmod(i, n) + qubit_index = old_num_qubits + qubit + self._var2op[v] = (qubit_index, self._ops[op]) + self._q2vars[qubit_index].append(v) + + def _add_term(self, w: float, *variables: int) -> None: + """Add a term to the Hamiltonian. + + Args: + weight: the coefficient for the term + *variables: the list of variables for the term + """ + # Eq. (31) in https://arxiv.org/abs/2111.03167 assumes a weight-2 + # Pauli operator. To generalize, we replace the `d` in that equation + # with `d_prime`, defined as follows: + d_prime = np.sqrt(self.max_vars_per_qubit) ** len(variables) + op = self._term2op(*variables) * (w * d_prime) + + if w == 0.0: + return + if self._qubit_op is None: + self._qubit_op = op + else: + self._qubit_op += op + + def _term2op(self, *variables: int) -> SparsePauliOp: + """Construct a ``SparsePauliOp`` that is a tensor product of encoded decision variables. + + Args: + *variables: The indices of the decision variables to encode. + + Returns: + The encoded ``SparsePauliOp`` representing the product of the provided variables. + + Raises: + QiskitOptimizationError: If any of the decision variables to be encoded collide in qubit + space. + + """ + ops = [SparsePauliOp("I")] * self.num_qubits + done = set() + for x in variables: + pos, op = self._var2op[x] + if pos in done: + raise QiskitOptimizationError(f"Collision of variables: {variables}") + ops[pos] = op + done.add(pos) + pauli_op = reduce(lambda x, y: x.tensor(y), ops) + return pauli_op + + @staticmethod + def _generate_ising_coefficients( + problem: QuadraticProgram, + ) -> tuple[float, np.ndarray, np.ndarray]: + """Generate coefficients of Hamiltonian from a given problem.""" + num_vars = problem.get_num_vars() + + # set a sign corresponding to a maximized or minimized problem: + # 1 is for minimized problem, -1 is for maximized problem. + sense = problem.objective.sense.value + + # convert a constant part of the objective function into Hamiltonian. + offset = problem.objective.constant * sense + + # convert linear parts of the objective function into Hamiltonian. + linear = np.zeros(num_vars) + for idx, coef in problem.objective.linear.to_dict().items(): + idx = cast(int, idx) + weight = coef * sense / 2 + linear[idx] -= weight + offset += weight + + # convert quadratic parts of the objective function into Hamiltonian. + quad = np.zeros((num_vars, num_vars)) + for (i, j), coef in problem.objective.quadratic.to_dict().items(): + i = cast(int, i) + j = cast(int, j) + weight = coef * sense / 4 + if i == j: + linear[i] -= 2 * weight + offset += 2 * weight + else: + quad[i, j] += weight + linear[i] -= weight + linear[j] -= weight + offset += weight + + return offset, linear, quad + + @staticmethod + def _find_variable_partition(quad: np.ndarray) -> dict[int, list[int]]: + """Find the variable partition of the quad based on the node coloring of the graph + + Args: + coefficients of the quadratic part of the Hamiltonian. + + Returns: + A dictionary of the variable partition of the quad based on the node coloring. + """ + # pylint: disable=E1101 + color2node: dict[int, list[int]] = defaultdict(list) + num_nodes = quad.shape[0] + graph = nx.Graph() + graph.add_nodes_from(range(num_nodes)) + graph.add_edges_from(list(zip(*np.where(quad != 0)))) + node2color = nx.greedy_color(graph) + for node, color in sorted(node2color.items()): + color2node[color].append(node) + return color2node + + def encode(self, problem: QuadraticProgram) -> None: + """ + Encodes a given ``QuadraticProgram`` as a (n,1,p) Quantum Random Access Code (QRAC) + relaxed Hamiltonian. It accomplishes this by mapping each binary decision variable to one + qubit of the QRAC. The encoding is designed to ensure that the problem's objective function + commutes with the QRAC encoding. + + After the function is called, it sets the following attributes: + - qubit_op: The qubit operator that encodes the input ``QuadraticProgram``. + - offset: The constant value in the encoded Hamiltonian. + - problem: The original ``QuadraticProgram`` used for encoding. + + Inputs: + problem: A ``QuadraticProgram`` encoding a QUBO optimization problem + + Raises: + QiskitOptimizationError: If this method is called more than once on the same object. + QiskitOptimizationError: If the problem contains non-binary variables. + QiskitOptimizationError: If the problem contains constraints. + """ + # Ensure the Encoding object is not already used + if self._frozen: + raise QiskitOptimizationError( + "Cannot reuse an Encoding object that has already been used. " + "Please create a new Encoding object and call encode() on it." + ) + + # Check for non-binary variables + if problem.get_num_vars() > problem.get_num_binary_vars(): + raise QiskitOptimizationError( + "All variables must be binary. " + "Please convert integer variables to binary variables using the" + "``QuadraticProgramToQubo`` converter. " + "Continuous variables are not supported by the QRAO algorithm." + ) + + # Check for constraints + if problem.linear_constraints or problem.quadratic_constraints: + raise QiskitOptimizationError( + "The problem cannot contain constraints. " + "Please convert constraints to penalty terms of the objective function using the " + "``QuadraticProgramToQubo`` converter." + ) + + num_vars = problem.get_num_vars() + + # Generate the coefficients of the Hamiltonian + offset, linear, quad = self._generate_ising_coefficients(problem) + + # Find the partition of the variables into groups + variable_partition = self._find_variable_partition(quad) + + # Add variables and generate the Hamiltonian + for _, v in sorted(variable_partition.items()): + self._add_variables(sorted(v)) + for i in range(num_vars): + w = linear[i] + if w != 0: + self._add_term(w, i) + for i in range(num_vars): + for j in range(num_vars): + w = quad[i, j] + if w != 0: + self._add_term(w, i, j) + + self._offset = offset + self._problem = problem + + self.freeze() + + def state_preparation_circuit(self, x: list[int]) -> QuantumCircuit: + """ + Generate a circuit that prepares the state corresponding to the given binary string. + + Args: + x: A list of binary values to be encoded into the state. + + Returns: + A QuantumCircuit that prepares the state corresponding to the given binary string. + """ + return _qrac_state_prep_multi_qubit(x, self.q2vars, self.max_vars_per_qubit) diff --git a/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py b/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py new file mode 100644 index 000000000..9d41ab4f5 --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py @@ -0,0 +1,313 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Quantum Random Access Optimizer class.""" +from __future__ import annotations + +from typing import cast + +import numpy as np +from qiskit import QuantumCircuit +from qiskit_algorithms import ( + MinimumEigensolver, + MinimumEigensolverResult, + NumPyMinimumEigensolverResult, + VariationalResult, +) + +from qiskit_optimization.algorithms import ( + OptimizationAlgorithm, + OptimizationResult, + OptimizationResultStatus, + SolutionSample, +) +from qiskit_optimization.converters import QuadraticProgramToQubo +from qiskit_optimization.problems import QuadraticProgram, Variable + +from .quantum_random_access_encoding import QuantumRandomAccessEncoding +from .rounding_common import RoundingContext, RoundingResult, RoundingScheme +from .semideterministic_rounding import SemideterministicRounding + + +class QuantumRandomAccessOptimizationResult(OptimizationResult): + """Result of Quantum Random Access Optimization procedure.""" + + def __init__( + self, + *, + x: list[float] | np.ndarray, + fval: float, + variables: list[Variable], + status: OptimizationResultStatus, + samples: list[SolutionSample], + encoding: QuantumRandomAccessEncoding, + relaxed_fval: float, + relaxed_result: MinimumEigensolverResult, + rounding_result: RoundingResult, + ) -> None: + """ + Args: + x: The optimal value found by ``MinimumEigensolver``. + fval: The optimal function value. + variables: The list of variables of the optimization problem. + status: The termination status of the optimization algorithm. + samples: The list of ``SolutionSample`` obtained from the optimization algorithm. + encoding: The encoding used for the optimization. + relaxed_fval: The optimal function value of the relaxed problem. + relaxed_result: The result obtained from the underlying minimum eigensolver. + rounding_result: The rounding result. + """ + super().__init__( + x=x, + fval=fval, + variables=variables, + status=status, + raw_results=None, + samples=samples, + ) + self._encoding = encoding + self._relaxed_fval = relaxed_fval + self._relaxed_result = relaxed_result + self._rounding_result = rounding_result + + @property + def encoding(self) -> QuantumRandomAccessEncoding: + """The encoding used for the optimization.""" + return self._encoding + + @property + def relaxed_fval(self) -> float: + """The optimal function value of the relaxed problem.""" + return self._relaxed_fval + + @property + def relaxed_result( + self, + ) -> MinimumEigensolverResult: + """The result obtained from the underlying minimum eigensolver.""" + return self._relaxed_result + + @property + def rounding_result(self) -> RoundingResult: + """The rounding result.""" + return self._rounding_result + + +class QuantumRandomAccessOptimizer(OptimizationAlgorithm): + """Quantum Random Access Optimizer class.""" + + def __init__( + self, + min_eigen_solver: MinimumEigensolver, + max_vars_per_qubit: int = 3, + rounding_scheme: RoundingScheme | None = None, + *, + penalty: float | None = None, + ): + """ + Args: + min_eigen_solver: The minimum eigensolver to use for solving the relaxed problem. + max_vars_per_qubit: The maximum number of decision variables per qubit. + Integer values 1, 2 and 3 are supported (default to 3). + rounding_scheme: The rounding scheme. If ``None`` is provided, + :class:`~.SemideterministicRounding` will be used. + penalty: The penalty factor to use for the :class:`~.QuadraticProgramToQubo` converter. + + Raises: + ValueError: If the maximum number of variables per qubit is not 1, 2, or 3. + TypeError: If the provided minimum eigensolver does not support auxiliary operators. + """ + if max_vars_per_qubit not in (1, 2, 3): + raise ValueError("max_vars_per_qubit must be 1, 2, or 3, but was {max_vars_per_qubit}.") + self._max_vars_per_qubit = max_vars_per_qubit + self.min_eigen_solver = min_eigen_solver + # Use ``QuadraticProgramToQubo`` to convert the problem to a QUBO. + if rounding_scheme is None: + rounding_scheme = SemideterministicRounding() + self._rounding_scheme = rounding_scheme + self._converters = QuadraticProgramToQubo( + penalty=penalty, + ) + + @property + def min_eigen_solver(self) -> MinimumEigensolver: + """Return the minimum eigensolver.""" + return self._min_eigen_solver + + @min_eigen_solver.setter + def min_eigen_solver(self, min_eigen_solver: MinimumEigensolver) -> None: + """Set the minimum eigensolver.""" + if not min_eigen_solver.supports_aux_operators(): + raise TypeError( + f"The provided MinimumEigensolver ({type(min_eigen_solver)}) " + "does not support auxiliary operators." + ) + self._min_eigen_solver = min_eigen_solver + + @property + def max_vars_per_qubit(self) -> int: + """Return the maximum number of variables per qubit.""" + return self._max_vars_per_qubit + + @max_vars_per_qubit.setter + def max_vars_per_qubit(self, max_vars_per_qubit: int) -> None: + """Set the maximum number of variables per qubit.""" + self._max_vars_per_qubit = max_vars_per_qubit + + @property + def rounding_scheme(self) -> RoundingScheme: + """Return the rounding scheme.""" + return self._rounding_scheme + + @rounding_scheme.setter + def rounding_scheme(self, rounding_scheme: RoundingScheme) -> None: + """Set the rounding scheme.""" + self._rounding_scheme = rounding_scheme + + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem can be converted + to a QUBO, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + A message describing the incompatibility. + """ + return QuadraticProgramToQubo.get_compatibility_msg(problem) + + def solve_relaxed( + self, + encoding: QuantumRandomAccessEncoding, + ) -> tuple[MinimumEigensolverResult, RoundingContext]: + """Solve the relaxed Hamiltonian given by the encoding. + + .. note:: + This method uses the encoding instance given as ``encoding`` and + ignores :meth:`max_vars_per_qubit`. + + Args: + encoding: An encoding instance for which :meth:`~QuantumRandomAccessEncoding.encode` + has already been called so it has been encoded with a :class:`~.QuadraticProgram`. + + Returns: + The result of the minimum eigensolver, and the rounding context. + + Raises: + ValueError: If the encoding has not been encoded with a :class:`~.QuadraticProgram`. + """ + if not encoding.frozen: + raise ValueError( + "The encoding must call ``encode()`` with a ``QuadraticProgram`` before being passed" + "to the QuantumRandomAccessOptimizer." + ) + + # Get the list of operators that correspond to each decision variable. + variable_ops = [encoding._term2op(i) for i in range(encoding.num_vars)] + + # Solve the relaxed problem. + relaxed_result = self.min_eigen_solver.compute_minimum_eigenvalue( + encoding.qubit_op, aux_operators=variable_ops + ) + + # Get auxiliary expectation values for rounding. + expectation_values: list[complex] | None = None + if relaxed_result.aux_operators_evaluated is not None: + expectation_values = [v[0] for v in relaxed_result.aux_operators_evaluated] # type: ignore + + # Get the circuit corresponding to the relaxed solution. + if isinstance(relaxed_result, VariationalResult): + circuit = relaxed_result.optimal_circuit.bind_parameters(relaxed_result.optimal_point) + elif isinstance(relaxed_result, NumPyMinimumEigensolverResult): + statevector = relaxed_result.eigenstate + circuit = QuantumCircuit(encoding.num_qubits) + circuit.initialize(statevector) + else: + circuit = None + + rounding_context = RoundingContext( + encoding=encoding, + expectation_values=expectation_values, + circuit=circuit, + ) + + return relaxed_result, rounding_context + + def solve(self, problem: QuadraticProgram) -> QuantumRandomAccessOptimizationResult: + """Solve the relaxed Hamiltonian given by the encoding and round the solution by the given + rounding scheme. + + Args: + problem: The :class:`~.QuadraticProgram` to be solved. + + Returns: + The result of the quantum random access optimization. + + Raises: + ValueError: If the encoding has not been encoded with a :class:`~.QuadraticProgram`. + """ + # Convert the problem to a QUBO + self._verify_compatibility(problem) + qubo = self._convert(problem, self._converters) + # Encode the QUBO into a quantum random access encoding + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=self.max_vars_per_qubit) + encoding.encode(qubo) + + # Solve the relaxed problem + relaxed_result, rounding_context = self.solve_relaxed(encoding) + + # Round the solution + rounding_result = self.rounding_scheme.round(rounding_context) + + return self.process_result(problem, encoding, relaxed_result, rounding_result) + + def process_result( + self, + problem: QuadraticProgram, + encoding: QuantumRandomAccessEncoding, + relaxed_result: MinimumEigensolverResult, + rounding_result: RoundingResult, + ) -> QuantumRandomAccessOptimizationResult: + """Process the relaxed result of the minimum eigensolver and rounding scheme. + + Args: + problem: The :class:`~.QuadraticProgram` to be solved. + encoding: An encoding instance for which :meth:`~QuantumRandomAccessEncoding.encode` + has already been called so it has been encoded with a :class:`~.QuadraticProgram`. + relaxed_result: The relaxed result of the minimum eigensolver. + rounding_result: The result of the rounding scheme. + + Returns: + The result of the quantum random access optimization. + """ + samples, best_sol = self._interpret_samples( + problem=problem, raw_samples=rounding_result.samples + ) + + relaxed_fval = encoding.problem.objective.sense.value * ( + encoding.offset + relaxed_result.eigenvalue.real + ) + return cast( + QuantumRandomAccessOptimizationResult, + self._interpret( + x=best_sol.x, + problem=problem, + result_class=QuantumRandomAccessOptimizationResult, + samples=samples, + encoding=encoding, + relaxed_fval=relaxed_fval, + relaxed_result=relaxed_result, + rounding_result=rounding_result, + ), + ) diff --git a/qiskit_optimization/algorithms/qrao/rounding_common.py b/qiskit_optimization/algorithms/qrao/rounding_common.py new file mode 100644 index 000000000..9516dd92b --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/rounding_common.py @@ -0,0 +1,63 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Common classes for rounding schemes""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import numpy as np +from qiskit.circuit import QuantumCircuit + +from qiskit_optimization.algorithms import SolutionSample + +from .quantum_random_access_encoding import QuantumRandomAccessEncoding + + +@dataclass +class RoundingResult: + """Result of rounding""" + + expectation_values: list[complex] | None + """Expectation values""" + samples: list[SolutionSample] + """List of samples after rounding""" + bases: np.ndarray | None = None + """The bases used for the magic rounding""" + basis_shots: np.ndarray | None = None + """The number of shots used for each basis for the magic rounding""" + basis_counts: list[dict[str, int]] | None = None + """The basis_counts represents the resulting counts obtained by measuring with the bases + corresponding to the number of shots specified in basis_shots for the magic rounding.""" + + +@dataclass +class RoundingContext: + """Information that is provided for rounding""" + + encoding: QuantumRandomAccessEncoding + """Encoding containing the problem information.""" + expectation_values: list[complex] | None + """Expectation values for the relaxed Hamiltonian.""" + circuit: QuantumCircuit | None = None + """Circuit corresponding to the encoding and expectation values.""" + + +class RoundingScheme(ABC): + """Base class for a rounding scheme""" + + @abstractmethod + def round(self, rounding_context: RoundingContext) -> RoundingResult: + """Perform rounding + + Returns: an instance of RoundingResult + """ diff --git a/qiskit_optimization/algorithms/qrao/semideterministic_rounding.py b/qiskit_optimization/algorithms/qrao/semideterministic_rounding.py new file mode 100644 index 000000000..017a94066 --- /dev/null +++ b/qiskit_optimization/algorithms/qrao/semideterministic_rounding.py @@ -0,0 +1,80 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Semi-deterministic rounding module""" +from __future__ import annotations + +import numpy as np + +from qiskit_optimization.algorithms import OptimizationResultStatus, SolutionSample +from qiskit_optimization.exceptions import QiskitOptimizationError + +from .rounding_common import RoundingContext, RoundingResult, RoundingScheme + + +class SemideterministicRounding(RoundingScheme): + """Semi-deterministic rounding scheme + + This is referred to as "Pauli rounding" in + https://arxiv.org/abs/2111.03167. + """ + + def __init__(self, *, atol: float = 1e-8, seed: int | None = None): + """ + Args: + seed: Seed for random number generator, which is used to resolve + expectation values near zero to either +1 or -1. + atol: Absolute tolerance for determining whether an expectation value is zero. + """ + super().__init__() + self._rng = np.random.default_rng(seed) + self._atol = atol + + def round(self, rounding_context: RoundingContext) -> RoundingResult: + """Perform semi-deterministic rounding + + Args: + rounding_context: Rounding context containing information about the problem and solution. + + Returns: + Result containing the rounded solution. + + Raises: + QiskitOptimizationError: If the expectation values are not available in the context. + """ + if rounding_context.expectation_values is None: + raise QiskitOptimizationError( + "Semi-deterministic rounding requires the expectation values of the ", + "``RoundingContext`` to be available, but they are not.", + ) + + rounded_vars = np.where( + np.isclose(rounding_context.expectation_values, 0, atol=self._atol), + self._rng.integers(2, size=len(rounding_context.expectation_values)), + np.less_equal(rounding_context.expectation_values, 0).astype(int), + ) + + soln_samples = [ + SolutionSample( + x=np.asarray(rounded_vars), + fval=rounding_context.encoding.problem.objective.evaluate(rounded_vars), + probability=1.0, + status=OptimizationResultStatus.SUCCESS + if rounding_context.encoding.problem.is_feasible(rounded_vars) + else OptimizationResultStatus.INFEASIBLE, + ) + ] + + result = RoundingResult( + expectation_values=rounding_context.expectation_values, samples=soln_samples + ) + return result diff --git a/releasenotes/notes/qrao-89d5ff1d2927de64.yaml b/releasenotes/notes/qrao-89d5ff1d2927de64.yaml new file mode 100644 index 000000000..863addd3d --- /dev/null +++ b/releasenotes/notes/qrao-89d5ff1d2927de64.yaml @@ -0,0 +1,62 @@ +--- +features: + - | + Added a new optimization algorithm, :class:`~.QuantumRandomAccessOptimizer`. This approach + incorporates Quantum Random Access Codes (QRACs) as a tool to encode multiple classical binary + variables into a single qubit, thereby saving quantum resources and enabling exploration of + larger problem instances on a quantum computer. The encodings produce a local quantum + Hamiltonian whose ground state can be approximated with standard algorithms such as VQE, + and then rounded to yield approximation solutions of the original problem. + + :class:`~.QuantumRandomAccessOptimizer` has two methods for solving problems, + :meth:`~.QuantumRandomAccessOptimizer.solve` and + :meth:`~.QuantumRandomAccessOptimizer.solve_relaxed`. The solve method provides a seamless + workflow by automatically managing the encoding and rounding procedures, as demonstrated in the + example below. This allows for a simplified and streamlined user experience. + On the other hand, the solve_relaxed method offers the flexibility to break the computation + process into distinct steps. This feature can be advantageous when we need to compare solutions + obtained from different rounding schemes applied to a potential ground state. + + + For example: + + .. code-block:: python + + from qiskit_algorithms.optimizers import COBYLA + from qiskit_algorithms import VQE + from qiskit.circuit.library import RealAmplitudes + from qiskit.primitives import Estimator + + from qiskit_optimization.algorithms.qrao import ( + QuantumRandomAccessOptimizer, + QuantumRandomAccessEncoding, + SemideterministicRounding, + ) + from qiskit_optimization.problems import QuadraticProgram + + problem = QuadraticProgram() + problem.binary_var("x") + problem.binary_var("y") + problem.binary_var("z") + problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + + ansatz = RealAmplitudes(1) + vqe = VQE( + ansatz=ansatz, + optimizer=COBYLA(), + estimator=Estimator(), + ) + # solve() automatically performs the encoding, optimization, and rounding + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + result = qrao.solve(problem) + + # solve_relaxed() only performs the optimization. The encoding and rounding must be done manually. + # encoding + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(problem) + # optimization + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + relaxed_results, rounding_context = qrao.solve_relaxed(encoding=encoding) + # rounding + rounding = SemideterministicRounding() + result = rounding.round(rounding_context) diff --git a/test/algorithms/qrao/__init__.py b/test/algorithms/qrao/__init__.py new file mode 100644 index 000000000..26f7536d3 --- /dev/null +++ b/test/algorithms/qrao/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/algorithms/qrao/test_magic_rounding.py b/test/algorithms/qrao/test_magic_rounding.py new file mode 100644 index 000000000..8a7e77f74 --- /dev/null +++ b/test/algorithms/qrao/test_magic_rounding.py @@ -0,0 +1,338 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for MagicRounding""" +import unittest +from test.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np +from qiskit.circuit import QuantumCircuit +from qiskit.primitives import Sampler +from qiskit_algorithms import NumPyMinimumEigensolver + +from qiskit_optimization.algorithms import OptimizationResultStatus, SolutionSample +from qiskit_optimization.algorithms.qrao import ( + MagicRounding, + QuantumRandomAccessEncoding, + QuantumRandomAccessOptimizer, + RoundingContext, + RoundingResult, +) +from qiskit_optimization.problems import QuadraticProgram + + +class TestMagicRounding(QiskitOptimizationTestCase): + """MagicRounding tests.""" + + def setUp(self): + """Set up for all tests.""" + super().setUp() + self.problem = QuadraticProgram() + self.problem.binary_var("x") + self.problem.binary_var("y") + self.problem.binary_var("z") + self.problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + + def test_magic_rounding_constructor(self): + """Test constructor""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + # test default + magic_rounding = MagicRounding(sampler) + self.assertEqual(magic_rounding.sampler, sampler) + self.assertEqual(magic_rounding.basis_sampling, "uniform") + # test weighted basis sampling + magic_rounding = MagicRounding(sampler, basis_sampling="weighted") + self.assertEqual(magic_rounding.sampler, sampler) + self.assertEqual(magic_rounding.basis_sampling, "weighted") + # test uniform basis sampling + magic_rounding = MagicRounding(sampler, basis_sampling="uniform") + self.assertEqual(magic_rounding.sampler, sampler) + self.assertEqual(magic_rounding.basis_sampling, "uniform") + # test invalid basis sampling + with self.assertRaises(ValueError): + MagicRounding(sampler, basis_sampling="invalid") + + def test_magic_rounding_round_uniform_1_1_qrac(self): + """Test round method with uniform basis sampling for max_vars_per_qubit=1""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=1) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + magic_rounding = MagicRounding(sampler, seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0, 0, 0]]) + np.testing.assert_allclose(rounding_result.basis_shots, [10000]) + expected_basis_counts = [{"000": 10000}] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=1, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.05) + np.testing.assert_allclose( + rounding_result.expectation_values, + [1, 1, 1], + ) + + def test_magic_rounding_round_weighted_1_1_qrac(self): + """Test round method with uniform basis sampling for max_vars_per_qubit=1""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=1) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + magic_rounding = MagicRounding(sampler, basis_sampling="weighted", seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0, 0, 0]]) + np.testing.assert_allclose(rounding_result.basis_shots, [10000]) + expected_basis_counts = [{"000": 10000}] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=1, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.05) + np.testing.assert_allclose( + rounding_result.expectation_values, + [1, 1, 1], + ) + + def test_magic_rounding_round_uniform_2_1_qrac(self): + """Test round method with uniform basis sampling for max_vars_per_qubit=2""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=2) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + magic_rounding = MagicRounding(sampler, seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0, 0], [0, 1], [1, 0], [1, 1]]) + np.testing.assert_allclose(rounding_result.basis_shots, [2575, 2482, 2440, 2503]) + expected_basis_counts = [ + {"00": 2154.0, "01": 367, "10": 44.0, "11": 10.0}, + {"00": 2076.0, "01": 357.0, "10": 45.0, "11": 4.0}, + {"00": 689.0, "01": 137.0, "10": 1401.0, "11": 213.0}, + {"00": 708.0, "01": 110.0, "10": 1446.0, "11": 239.0}, + ] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=0.423, problem=self.problem), + make_solution_sample(x=np.array([0, 0, 1]), probability=0.0724, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 0]), probability=0.1397, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 1]), probability=0.0247, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 0]), probability=0.2847, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 1]), probability=0.0452, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 0]), probability=0.0089, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 1]), probability=0.0014, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.05) + np.testing.assert_allclose( + rounding_result.expectation_values, + [0.44721359549995743, 0.8944271909999162, 1], + ) + + def test_magic_rounding_round_weighted_2_1_qrac(self): + """Test round method with weighted basis sampling for max_vars_per_qubit=2""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=2) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + magic_rounding = MagicRounding(sampler, basis_sampling="weighted", seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0, 0], [1, 0]]) + np.testing.assert_allclose(rounding_result.basis_shots, [7058, 2942]) + expected_basis_counts = [ + {"00": 5858.0, "01": 1036.0, "10": 137.0, "11": 27.0}, + {"00": 832.0, "01": 131.0, "10": 1698.0, "11": 281.0}, + ] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=0.5858, problem=self.problem), + make_solution_sample(x=np.array([0, 0, 1]), probability=0.1036, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 0]), probability=0.0832, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 1]), probability=0.0131, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 0]), probability=0.1698, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 1]), probability=0.0281, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 0]), probability=0.0137, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 1]), probability=0.0027, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.05) + np.testing.assert_allclose( + rounding_result.expectation_values, + [0.44721359549995743, 0.8944271909999162, 1], + ) + + def test_magic_rounding_round_uniform_3_1_qrac(self): + """Test round method with uniform basis sampling for max_vars_per_qubit=3""" + sampler = Sampler(options={"shots": 10000, "seed": 42}) + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + magic_rounding = MagicRounding(sampler, seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0], [1], [2], [3]]) + np.testing.assert_allclose(rounding_result.basis_shots, [2534, 2527, 2486, 2453]) + expected_basis_counts = [ + {"0": 2434.0, "1": 100.0}, + {"0": 469.0, "1": 2058.0}, + {"0": 833.0, "1": 1653.0}, + {"0": 1219.0, "1": 1234.0}, + ] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=0.2434, problem=self.problem), + make_solution_sample(x=np.array([0, 0, 1]), probability=0.1234, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 0]), probability=0.1653, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 1]), probability=0.0469, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 0]), probability=0.2058, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 1]), probability=0.0833, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 0]), probability=0.1219, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 1]), probability=0.01, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.05) + np.testing.assert_allclose( + rounding_result.expectation_values, + [0.2672612419124245, 0.5345224838248487, 0.8017837257372733], + ) + + def test_magic_rounding_round_weighted_3_1_qrac(self): + """Test round method with weighted basis sampling for max_vars_per_qubit=3""" + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(self.problem) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + _, rounding_context = qrao.solve_relaxed(encoding=encoding) + sampler = Sampler(options={"shots": 10000, "seed": 42}) + magic_rounding = MagicRounding(sampler, basis_sampling="weighted", seed=42) + rounding_result = magic_rounding.round(rounding_context) + self.assertIsInstance(rounding_result, RoundingResult) + np.testing.assert_allclose(rounding_result.bases, [[0], [1], [2], [3]]) + np.testing.assert_allclose(rounding_result.basis_shots, [4499, 2700, 1574, 1227]) + expected_basis_counts = [ + {"0": 4352.0, "1": 147.0}, + {"0": 500.0, "1": 2200.0}, + {"0": 528.0, "1": 1046.0}, + {"0": 630.0, "1": 597.0}, + ] + for i, basis_counts in enumerate(rounding_result.basis_counts): + for key, value in basis_counts.items(): + self.assertAlmostEqual(value, expected_basis_counts[i][key], delta=50) + samples = rounding_result.samples + samples.sort(key=lambda sample: np.array2string(sample.x)) + expected_samples = [ + make_solution_sample(x=np.array([0, 0, 0]), probability=0.4352, problem=self.problem), + make_solution_sample(x=np.array([0, 0, 1]), probability=0.0597, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 0]), probability=0.1046, problem=self.problem), + make_solution_sample(x=np.array([0, 1, 1]), probability=0.05, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 0]), probability=0.22, problem=self.problem), + make_solution_sample(x=np.array([1, 0, 1]), probability=0.0528, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 0]), probability=0.063, problem=self.problem), + make_solution_sample(x=np.array([1, 1, 1]), probability=0.0147, problem=self.problem), + ] + for i, sample in enumerate(samples): + np.testing.assert_allclose(sample.x, expected_samples[i].x) + self.assertAlmostEqual(sample.probability, expected_samples[i].probability, delta=0.005) + np.testing.assert_allclose( + rounding_result.expectation_values, + [0.2672612419124245, 0.5345224838248487, 0.8017837257372733], + ) + + def test_magic_rounding_exceptions(self): + """Test exceptions in the MagicRounding class""" + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(self.problem) + + with self.assertRaises(ValueError): + # circuit is None + sampler = Sampler(options={"shots": 10000, "seed": 42}) + magic_rounding = MagicRounding(sampler=sampler) + rounding_context = RoundingContext(encoding, expectation_values=[1, 1, 1], circuit=None) + magic_rounding.round(rounding_context) + + with self.assertRaises(ValueError): + # sampler without shots + sampler = Sampler() + magic_rounding = MagicRounding(sampler=sampler) + + with self.assertRaises(ValueError): + # expectation_values is None for weighted basis sampling + sampler = Sampler() + magic_rounding = MagicRounding(sampler=sampler, basis_sampling="weighted") + rounding_context = RoundingContext( + encoding, expectation_values=None, circuit=QuantumCircuit(1) + ) + magic_rounding.round(rounding_context) + + with self.assertRaises(ValueError): + # vars_per_qubit is invalid + sampler = Sampler(options={"shots": 10000, "seed": 42}) + magic_rounding = MagicRounding(sampler=sampler) + magic_rounding._make_circuits(circuit=QuantumCircuit(1), bases=[[0]], vars_per_qubit=4) + + +def make_solution_sample( + x: np.ndarray, probability: float, problem: QuadraticProgram +) -> SolutionSample: + """Make a solution sample.""" + return SolutionSample( + x=x, + fval=problem.objective.evaluate(x), + probability=probability, + status=OptimizationResultStatus.SUCCESS + if problem.is_feasible(x) + else OptimizationResultStatus.INFEASIBLE, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/algorithms/qrao/test_quantum_random_access_encoding.py b/test/algorithms/qrao/test_quantum_random_access_encoding.py new file mode 100644 index 000000000..b15b680d6 --- /dev/null +++ b/test/algorithms/qrao/test_quantum_random_access_encoding.py @@ -0,0 +1,331 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for QuantumRandomAccessEncoding""" +import itertools +import unittest +from test.optimization_test_case import QiskitOptimizationTestCase + +from ddt import ddt, data, unpack +import numpy as np +import networkx as nx + +from qiskit.circuit import QuantumCircuit +from qiskit.primitives import Estimator +from qiskit.quantum_info import SparsePauliOp + +from qiskit_optimization.algorithms.qrao import ( + EncodingCommutationVerifier, + QuantumRandomAccessEncoding, +) +from qiskit_optimization.problems import QuadraticProgram, QuadraticObjective +from qiskit_optimization.applications import Maxcut + + +class TestQuantumRandomAccessEncoding(QiskitOptimizationTestCase): + """QuantumRandomAccessEncoding tests.""" + + def setUp(self): + super().setUp() + self.problem = QuadraticProgram() + self.problem.binary_var("x") + self.problem.binary_var("y") + self.problem.binary_var("z") + self.problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + # quadratic objective + self.problem2 = QuadraticProgram() + self.problem2.binary_var("x") + self.problem2.binary_var("y") + self.problem2.binary_var("z") + self.problem2.maximize(linear={"x": 1, "y": 2, "z": 3}, quadratic={("y", "z"): -4}) + + def test_31p_qrac_encoding(self): + """Test (3,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(3) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem) + expected_op = SparsePauliOp( + ["X", "Y", "Z"], coeffs=[-np.sqrt(3) / 2, 2 * -np.sqrt(3) / 2, 3 * -np.sqrt(3) / 2] + ) + + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 1) + self.assertEqual(encoding.offset, 3) + self.assertEqual(encoding.max_vars_per_qubit, 3) + self.assertEqual(encoding.q2vars, [[0, 1, 2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["X"], coeffs=[1.0])), + 1: (0, SparsePauliOp(["Y"], coeffs=[1.0])), + 2: (0, SparsePauliOp(["Z"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 3) + self.assertEqual(encoding.minimum_recovery_probability, (1 + 1 / np.sqrt(3)) / 2) + self.assertEqual(encoding.problem, self.problem) + + def test_31p_qrac_encoding_quadratic(self): + """Test (3,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(3) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem2) + expected_op = SparsePauliOp(["XI", "IX", "YX"], coeffs=[np.sqrt(3) / 2, np.sqrt(3) / 2, 3]) + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 2) + self.assertEqual(encoding.offset, -2) + self.assertEqual(encoding.max_vars_per_qubit, 3) + self.assertEqual(encoding.q2vars, [[0, 1], [2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["X"], coeffs=[1.0])), + 1: (0, SparsePauliOp(["Y"], coeffs=[1.0])), + 2: (1, SparsePauliOp(["X"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 1.5) + self.assertEqual(encoding.minimum_recovery_probability, (1 + 1 / np.sqrt(3)) / 2) + self.assertEqual(encoding.problem, self.problem2) + + def test_21p_qrac_encoding(self): + """Test (2,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(2) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem) + expected_op = SparsePauliOp( + ["XI", "ZI", "IX"], + coeffs=[-np.sqrt(2) / 2, 2 * -np.sqrt(2) / 2, 3 * -np.sqrt(2) / 2], + ) + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 2) + self.assertEqual(encoding.offset, 3) + self.assertEqual(encoding.max_vars_per_qubit, 2) + self.assertEqual(encoding.q2vars, [[0, 1], [2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["X"], coeffs=[1.0])), + 1: (0, SparsePauliOp(["Z"], coeffs=[1.0])), + 2: (1, SparsePauliOp(["X"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 1.5) + self.assertEqual(encoding.minimum_recovery_probability, (1 + 1 / np.sqrt(2)) / 2) + self.assertEqual(encoding.problem, self.problem) + + def test_21p_qrac_encoding_quadratic(self): + """Test (2,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(2) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem2) + expected_op = SparsePauliOp( + ["XI", "IX", "ZX"], + coeffs=[np.sqrt(2) / 2, np.sqrt(2) / 2, 2], + ) + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 2) + self.assertEqual(encoding.offset, -2) + self.assertEqual(encoding.max_vars_per_qubit, 2) + self.assertEqual(encoding.q2vars, [[0, 1], [2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["X"], coeffs=[1.0])), + 1: (0, SparsePauliOp(["Z"], coeffs=[1.0])), + 2: (1, SparsePauliOp(["X"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 1.5) + self.assertEqual(encoding.minimum_recovery_probability, (1 + 1 / np.sqrt(2)) / 2) + self.assertEqual(encoding.problem, self.problem2) + + def test_11p_qrac_encoding(self): + """Test (1,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(1) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem) + expected_op = SparsePauliOp(["ZII", "IZI", "IIZ"], coeffs=[-0.5, -1.0, -1.5]) + + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 3) + self.assertEqual(encoding.offset, 3) + self.assertEqual(encoding.max_vars_per_qubit, 1) + self.assertEqual(encoding.q2vars, [[0], [1], [2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["Z"], coeffs=[1.0])), + 1: (1, SparsePauliOp(["Z"], coeffs=[1.0])), + 2: (2, SparsePauliOp(["Z"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 1) + self.assertEqual(encoding.minimum_recovery_probability, 1) + self.assertEqual(encoding.problem, self.problem) + + def test_11p_qrac_encoding_quadratic(self): + """Test (1,1,p) QRAC""" + encoding = QuantumRandomAccessEncoding(1) + self.assertFalse(encoding.frozen) # frozen is False + encoding.encode(self.problem2) + expected_op = SparsePauliOp(["ZII", "IIZ", "IZZ"], coeffs=[0.5, 0.5, 1]) + + self.assertTrue(encoding.frozen) # frozen is True + self.assertEqual(encoding.qubit_op, expected_op) + self.assertEqual(encoding.num_vars, 3) + self.assertEqual(encoding.num_qubits, 3) + self.assertEqual(encoding.offset, -2) + self.assertEqual(encoding.max_vars_per_qubit, 1) + self.assertEqual(encoding.q2vars, [[0], [1], [2]]) + self.assertEqual( + encoding.var2op, + { + 0: (0, SparsePauliOp(["Z"], coeffs=[1.0])), + 1: (1, SparsePauliOp(["Z"], coeffs=[1.0])), + 2: (2, SparsePauliOp(["Z"], coeffs=[1.0])), + }, + ) + self.assertEqual(encoding.compression_ratio, 1) + self.assertEqual(encoding.minimum_recovery_probability, 1) + self.assertEqual(encoding.problem, self.problem2) + + def test_qrac_state_prep(self): + """Test that state preparation circuit is correct""" + dvars = [0, 1, 1] + with self.subTest(msg="(3,1,p) QRAC"): + encoding = QuantumRandomAccessEncoding(3) + encoding.encode(self.problem) + state_prep_circ = encoding.state_preparation_circuit(x=dvars) + circ = QuantumCircuit(1) + beta = np.arccos(1 / np.sqrt(3)) + circ.r(np.pi - beta, np.pi / 4, 0) + self.assertEqual(state_prep_circ, circ) + + with self.subTest(msg="(2,1,p) QRAC"): + encoding = QuantumRandomAccessEncoding(2) + encoding.encode(self.problem) + state_prep_circ = encoding.state_preparation_circuit(x=dvars) + circ = QuantumCircuit(2) + circ.x(0) + circ.r(-3 * np.pi / 4, -np.pi / 2, 0) + circ.r(-3 * np.pi / 4, -np.pi / 2, 1) + self.assertEqual(state_prep_circ, circ) + + with self.subTest(msg="(1,1,p) QRAC"): + encoding = QuantumRandomAccessEncoding(1) + encoding.encode(self.problem) + state_prep_circ = encoding.state_preparation_circuit(x=dvars) + circ = QuantumCircuit(3) + circ.x(0) + circ.x(1) + self.assertEqual(state_prep_circ, circ) + + def test_qrac_unsupported_encoding(self): + """Test that exception is raised if ``max_vars_per_qubit`` is invalid""" + with self.assertRaises(ValueError): + QuantumRandomAccessEncoding(4) + with self.assertRaises(ValueError): + QuantumRandomAccessEncoding(0) + + +@ddt +class TestEncodingCommutationVerifier(QiskitOptimizationTestCase): + """Tests for EncodingCommutationVerifier.""" + + def check_problem_commutation(self, problem: QuadraticProgram, max_vars_per_qubit: int): + """Utility function to check that the problem commutes with its encoding""" + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=max_vars_per_qubit) + encoding.encode(problem) + estimator = Estimator() + verifier = EncodingCommutationVerifier(encoding, estimator) + self.assertEqual(len(verifier), 2**encoding.num_vars) + for _, obj_val, encoded_obj_val in verifier: + np.testing.assert_allclose(obj_val, encoded_obj_val, atol=1e-5) + + def test_encoding_commutation_verifier(self): + """Test EncodingCommutationVerifier""" + problem = QuadraticProgram() + problem.binary_var("x") + problem.binary_var("y") + problem.binary_var("z") + problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + + encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + encoding.encode(problem) + self.check_problem_commutation(problem, 3) + + @data(*itertools.product([1, 2, 3], ["minimize", "maximize"])) + @unpack + def test_one_qubit_qrac(self, max_vars_per_qubit, task): + """Test commutation of single qubit QRAC with non-uniform weights, degree 1 terms""" + + problem = QuadraticProgram() + nodes = list(range(max_vars_per_qubit)) + _ = [problem.binary_var(name=f"x{i}") for i in nodes] + obj = {f"x{i}": 2 * (i + 1) for i in nodes} + if task == "minimize": + problem.minimize(linear=obj) + else: + problem.maximize(linear=obj) + self.check_problem_commutation(problem, max_vars_per_qubit) + + @data( + *itertools.product( + [1, 2, 3], [QuadraticObjective.Sense.MINIMIZE, QuadraticObjective.Sense.MAXIMIZE] + ) + ) + @unpack + def test_uniform_weights_degree_2(self, max_vars_per_qubit, task): + """Test problem commutation with degree 2 terms""" + # Note that the variable embedding has some qubits with 1, 2, and 3 qubits + elist = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (0, 3), (1, 4), (2, 4)] + graph = nx.from_edgelist(elist) + for w, v in elist: + graph[w][v]["weight"] = (w + 1) * (v + 2) + + maxcut = Maxcut(graph) + problem = maxcut.to_quadratic_program() + problem.objective.sense = task + self.check_problem_commutation(problem, max_vars_per_qubit) + + @data(1, 2, 3) + def test_random_unweighted_maxcut(self, max_vars_per_qubit): + """Test problem commutation with random unweighted MaxCut""" + graph = nx.random_regular_graph(3, 8) + maxcut = Maxcut(graph) + problem = maxcut.to_quadratic_program() + self.check_problem_commutation(problem, max_vars_per_qubit) + + @data(1, 2, 3) + def test_random_weighted_maxcut(self, max_vars_per_qubit): + """Test problem commutation with random weighted MaxCut""" + graph = nx.random_regular_graph(3, 8) + for w, v in graph.edges: + graph[w][v]["weight"] = np.random.randint(1, 10) + maxcut = Maxcut(graph) + problem = maxcut.to_quadratic_program() + self.check_problem_commutation(problem, max_vars_per_qubit) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/algorithms/qrao/test_quantum_random_access_optimizer.py b/test/algorithms/qrao/test_quantum_random_access_optimizer.py new file mode 100644 index 000000000..ae797e2fe --- /dev/null +++ b/test/algorithms/qrao/test_quantum_random_access_optimizer.py @@ -0,0 +1,195 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for QuantumRandomAccessOptimizer""" +import unittest +from test.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np +from qiskit_algorithms import ( + NumPyMinimumEigensolver, + NumPyMinimumEigensolverResult, + VQE, + VQEResult, +) +from qiskit_algorithms.optimizers import COBYLA +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Estimator +from qiskit.utils import algorithm_globals + +from qiskit_optimization.algorithms import SolutionSample +from qiskit_optimization.algorithms.optimization_algorithm import OptimizationResultStatus +from qiskit_optimization.algorithms.qrao import ( + QuantumRandomAccessEncoding, + QuantumRandomAccessOptimizationResult, + QuantumRandomAccessOptimizer, + RoundingContext, + RoundingResult, +) +from qiskit_optimization.problems import QuadraticProgram + + +class TestQuantumRandomAccessOptimizer(QiskitOptimizationTestCase): + """QuantumRandomAccessOptimizer tests.""" + + def setUp(self): + super().setUp() + self.problem = QuadraticProgram() + self.problem.binary_var("x") + self.problem.binary_var("y") + self.problem.binary_var("z") + self.problem.minimize(linear={"x": 1, "y": 2, "z": 3}) + self.encoding = QuantumRandomAccessEncoding(max_vars_per_qubit=3) + self.encoding.encode(self.problem) + self.ansatz = RealAmplitudes(self.encoding.num_qubits) # for VQE + algorithm_globals.random_seed = 50 + + def test_solve_relaxed_numpy(self): + """Test QuantumRandomAccessOptimizer with NumPyMinimumEigensolver.""" + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + relaxed_results, rounding_context = qrao.solve_relaxed(encoding=self.encoding) + self.assertIsInstance(relaxed_results, NumPyMinimumEigensolverResult) + self.assertAlmostEqual(relaxed_results.eigenvalue, -3.24037, places=5) + self.assertEqual(len(relaxed_results.aux_operators_evaluated), 3) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[0][0], 0.26726, places=5) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[1][0], 0.53452, places=5) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[2][0], 0.80178, places=5) + self.assertIsInstance(rounding_context, RoundingContext) + self.assertEqual(rounding_context.circuit.num_qubits, self.ansatz.num_qubits) + self.assertEqual(rounding_context.encoding, self.encoding) + self.assertAlmostEqual(rounding_context.expectation_values[0], 0.26726, places=5) + self.assertAlmostEqual(rounding_context.expectation_values[1], 0.53452, places=5) + self.assertAlmostEqual(rounding_context.expectation_values[2], 0.80178, places=5) + + def test_solve_relaxed_vqe(self): + """Test QuantumRandomAccessOptimizer with VQE.""" + vqe = VQE( + ansatz=self.ansatz, + optimizer=COBYLA(), + estimator=Estimator(), + ) + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + relaxed_results, rounding_context = qrao.solve_relaxed(encoding=self.encoding) + self.assertIsInstance(relaxed_results, VQEResult) + self.assertAlmostEqual(relaxed_results.eigenvalue, -2.73861, delta=1e-4) + self.assertEqual(len(relaxed_results.aux_operators_evaluated), 3) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[0][0], 0.31632, delta=1e-4) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[1][0], 0, delta=1e-4) + self.assertAlmostEqual(relaxed_results.aux_operators_evaluated[2][0], 0.94865, delta=1e-4) + self.assertIsInstance(rounding_context, RoundingContext) + self.assertEqual(rounding_context.circuit.num_qubits, self.ansatz.num_qubits) + self.assertEqual(rounding_context.encoding, self.encoding) + self.assertAlmostEqual(rounding_context.expectation_values[0], 0.31632, delta=1e-4) + self.assertAlmostEqual(rounding_context.expectation_values[1], 0, delta=1e-4) + self.assertAlmostEqual(rounding_context.expectation_values[2], 0.94865, delta=1e-4) + + def test_require_aux_operator_support(self): + """Test whether the eigensolver supports auxiliary operator. + If auxiliary operators are not supported, a TypeError should be raised. + """ + + class ModifiedVQE(VQE): + """Modified VQE method without auxiliary operator support. + Since no existing eigensolver seems to lack auxiliary operator support, + we have created one that claims to lack it. + """ + + @classmethod + def supports_aux_operators(cls) -> bool: + return False + + vqe = ModifiedVQE( + ansatz=self.ansatz, + optimizer=COBYLA(), + estimator=Estimator(), + ) + with self.assertRaises(TypeError): + QuantumRandomAccessOptimizer(min_eigen_solver=vqe) + + def test_solve_numpy(self): + """Test QuantumRandomAccessOptimizer with NumPyMinimumEigensolver.""" + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + results = qrao.solve(problem=self.problem) + self.assertIsInstance(results, QuantumRandomAccessOptimizationResult) + self.assertEqual(results.fval, 0) + self.assertEqual(len(results.samples), 1) + np.testing.assert_array_almost_equal(results.samples[0].x, [0, 0, 0]) + self.assertAlmostEqual(results.samples[0].fval, 0) + self.assertAlmostEqual(results.samples[0].probability, 1.0) + self.assertEqual(results.samples[0].status, OptimizationResultStatus.SUCCESS) + self.assertAlmostEqual(results.relaxed_fval, -0.24037, places=5) + self.assertIsInstance(results.relaxed_result, NumPyMinimumEigensolverResult) + self.assertAlmostEqual(results.relaxed_result.eigenvalue, -3.24037, places=5) + self.assertEqual(len(results.relaxed_result.aux_operators_evaluated), 3) + self.assertAlmostEqual( + results.relaxed_result.aux_operators_evaluated[0][0], 0.26726, places=5 + ) + self.assertAlmostEqual( + results.relaxed_result.aux_operators_evaluated[1][0], 0.53452, places=5 + ) + self.assertAlmostEqual( + results.relaxed_result.aux_operators_evaluated[2][0], 0.80178, places=5 + ) + self.assertIsInstance(results.rounding_result, RoundingResult) + self.assertAlmostEqual(results.rounding_result.expectation_values[0], 0.26726, places=5) + self.assertAlmostEqual(results.rounding_result.expectation_values[1], 0.53452, places=5) + self.assertAlmostEqual(results.rounding_result.expectation_values[2], 0.80178, places=5) + self.assertIsInstance(results.rounding_result.samples[0], SolutionSample) + + def test_solve_quadratic(self): + """Test QuantumRandomAccessOptimizer with a quadratic objective function.""" + # quadratic objective + problem2 = QuadraticProgram() + problem2.binary_var("x") + problem2.binary_var("y") + problem2.binary_var("z") + problem2.maximize(linear={"x": 1, "y": 2, "z": 3}, quadratic={("y", "z"): -4}) + np_solver = NumPyMinimumEigensolver() + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + results = qrao.solve(problem2) + self.assertIsInstance(results, QuantumRandomAccessOptimizationResult) + self.assertEqual(results.fval, 4) + self.assertEqual(len(results.samples), 1) + np.testing.assert_array_almost_equal(results.samples[0].x, [1, 0, 1]) + self.assertAlmostEqual(results.samples[0].fval, 4) + self.assertAlmostEqual(results.samples[0].probability, 1.0) + self.assertEqual(results.samples[0].status, OptimizationResultStatus.SUCCESS) + self.assertAlmostEqual(results.relaxed_fval, -5.98852, places=5) + self.assertIsInstance(results.relaxed_result, NumPyMinimumEigensolverResult) + self.assertAlmostEqual(results.relaxed_result.eigenvalue, -3.98852, places=5) + self.assertEqual(len(results.relaxed_result.aux_operators_evaluated), 3) + self.assertAlmostEqual( + results.relaxed_result.aux_operators_evaluated[0][0], -0.27735, places=5 + ) + self.assertAlmostEqual( + results.relaxed_result.aux_operators_evaluated[1][0], 0.96077, places=5 + ) + self.assertAlmostEqual(results.relaxed_result.aux_operators_evaluated[2][0], -1, places=5) + self.assertIsInstance(results.rounding_result, RoundingResult) + self.assertAlmostEqual(results.rounding_result.expectation_values[0], -0.27735, places=5) + self.assertAlmostEqual(results.rounding_result.expectation_values[1], 0.96077, places=5) + self.assertAlmostEqual(results.rounding_result.expectation_values[2], -1, places=5) + self.assertIsInstance(results.rounding_result.samples[0], SolutionSample) + + def test_empty_encoding(self): + """Test the encoding is empty.""" + np_solver = NumPyMinimumEigensolver() + encoding = QuantumRandomAccessEncoding(3) + with self.assertRaises(ValueError): + qrao = QuantumRandomAccessOptimizer(min_eigen_solver=np_solver) + qrao.solve_relaxed(encoding=encoding) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/algorithms/qrao/test_semideterministic_rounding.py b/test/algorithms/qrao/test_semideterministic_rounding.py new file mode 100644 index 000000000..80c609ca8 --- /dev/null +++ b/test/algorithms/qrao/test_semideterministic_rounding.py @@ -0,0 +1,55 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for SemideterministicRounding""" +import unittest +from test.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np + +from qiskit_optimization.algorithms.qrao import ( + QuantumRandomAccessEncoding, + SemideterministicRounding, + RoundingResult, + RoundingContext, +) +from qiskit_optimization.algorithms import SolutionSample +from qiskit_optimization.problems import QuadraticProgram + + +class TestSemideterministicRounding(QiskitOptimizationTestCase): + """SemideterministicRounding tests.""" + + def setUp(self): + super().setUp() + self.problem = QuadraticProgram() + var_dict = self.problem.binary_var_dict(5) + self.problem.minimize(linear={name: 1 for name in var_dict}) + + def test_semideterministic_rounding(self): + """Test SemideterministicRounding""" + encoding = QuantumRandomAccessEncoding() + encoding.encode(self.problem) + rounding_scheme = SemideterministicRounding(seed=123) + expectation_values = [1, -1, 0, 0.7, -0.3] + result = rounding_scheme.round( + RoundingContext(expectation_values=expectation_values, encoding=encoding) + ) + self.assertIsInstance(result, RoundingResult) + self.assertIsInstance(result.samples[0], SolutionSample) + self.assertEqual(result.expectation_values, [1, -1, 0, 0.7, -0.3]) + np.testing.assert_array_almost_equal(result.samples[0].x, [0, 1, 1, 0, 1]) + self.assertEqual(result.samples[0].probability, 1.0) + + +if __name__ == "__main__": + unittest.main()