Source code for qrisp.algorithms.qaoa.mixers

"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""

import numpy as np
from scipy.optimize import minimize
from sympy import Symbol

from qrisp import QuantumVariable, h, barrier, rz, rx , cx, QuantumArray, xxyy, p, invert, conjugate, mcp, auto_uncompute, control

[docs] def RX_mixer(qv, beta): """ Applies an RX gate to each qubit in ``qv``. The RX gate is a single-qubit rotation about the x-axis. It is used as a mixer in QAOA to drive transitions between different states. Parameters ---------- qv : QuantumVariable The quantum variable to which the RX gate is applied. beta : float or sympy.Symbol The phase shift value for the RX gate. Returns ------- qv : QuantumVariable The quantum variable after applying the RX gate. """ for i in range(qv.size): rx(2 * beta, qv[i]) return qv
[docs] def XY_mixer(qv, beta): """ Applies multiple XX+YY gates to ``qv`` such that each qubit has interacted with it's neighbour at least once. The XX+YY gate is a two-qubit gate that performs rotations around the XY plane. It is used as a mixer in QAOA to drive transitions between different states. A defining feature of this mixer is the fact, that it keeps the number of ones (or equivalently zeros) in the binary representation of the state invariant. Parameters ---------- qv : QuantumVariable The quantum variable to which the XY gate is applied. beta : float or sympy.Symbol The phase shift value for the XY gate. Returns ------- qv : QuantumVariable The quantum variable after applying the XY gate. """ N = qv.size for i in range(0, N//2): q1 = qv[2*i] q2 = qv[2*i+1] xxyy(4*beta, 0, q1, q2) for i in range(0, (N-2+N%2)//2): q1 = qv[2*i+1] q2 = qv[2*i+2] xxyy(4*beta, 0, q1, q2) xxyy(4*beta, 0, qv[N-1], qv[0]) return qv
def apply_XY_mixer(quantumcolor_array, beta): for qcolor in quantumcolor_array: XY_mixer(qcolor, beta) return quantumcolor_array
[docs] def RZ_mixer(qv, beta): """ This function applies an RZ gate with a negative phase shift to a given quantum variable. Parameters ---------- qv : QuantumVariable The quantum variable to which the RZ gate is applied. beta : float or sympy.Symbol The phase shift value for the RZ gate. """ rz(-beta, qv)
[docs] def grover_mixer(qv, beta): """ Performs the parametrized Grover diffuser. Parameters ---------- qv : QuantumVariable The QuantumVariable to be mixed. beta : float or sympy.Symbol The mixing parameter. """ from qrisp.grover import diffuser diffuser(qv, phase = beta)
[docs] def constrained_mixer_gen(constraint_oracle, winner_state_amount): r""" Generates a customized mixer function that leaves arbitrary constraints intact. The constraints are specified via a ``constraint_oracle`` function, which is taking a :ref:`QuantumVariable` or :ref:`QuantumArray` and apply a phase $\phi$ (specified by the keyword argument ``phase``) to the states that are allowed by the constraints. Additionally the amount of winner states needs to be known. For this the user needs to provide the function ``winner_state_amount``, that returns the number of winner states for a given qubit amount. This number can be an approximation, however faulty values can cause leakage into the state-space that is forbidden by the constraints. For more details regarding implementation specifics please check the corresponding :ref:`tutorial <ConstrainedMixers>`. Parameters ---------- constraint_oracle : function A function of a :ref:`QuantumVariable` or :ref:`QuantumArray`. Also needs to support the keyword argument ``phase``. This function should apply the phase specified by the keyword argument to the allowed states. winner_state_amount : function A function of a QuantumVariable or QuantumArray, that returns the amount of winner states for that QuantumVariable. Returns ------- constrained_mixer : function A mixer function that does not leave the allowed space specified by the oracle. Examples -------- We create a mixer function that only mixes among the states where the first and the last qubit disagree. In more mathematical terms - they satisfy the following constraint function. .. math:: f: \mathbb{F}_2^n \rightarrow \mathbb{F}_2, x \rightarrow (x_{n-1} \neq x_0) :: from qrisp.qaoa import constrained_mixer_gen from qrisp import QuantumVariable, auto_uncompute, cx, p @auto_uncompute def constraint_oracle(qarg, phase): predicate = QuantumBool() cx(qarg[0], predicate) cx(qarg[-1], predicate) p(phase, predicate) def winner_state_amount(qarg): return 2**(len(qarg) - 1) mixer = constrained_mixer_gen(constraint_oracle, winner_state_amount) To test the mixer, we create a :ref:`QuantumVariable`: :: import numpy as np beta = np.pi qv = QuantumVariable(3) qv[:] = "101" mixer(qv, beta) print(qv) #Yields: {'101': 1.0} #Leaves forbidden states invariant qv = QuantumVariable(3) qv[:] = "100" mixer(qv, beta) print(qv) #Yields: {'100': 0.25, '110': 0.25, '001': 0.25, '011': 0.25} #Only mixes among allowed states """ from qrisp.grover import grovers_alg def prep_psi(qarg): if isinstance(qarg, QuantumVariable): qubit_amount = len(qarg) elif isinstance(qarg, QuantumArray): qubit_amount = len(qarg.qtype)*len(qarg.flatten()) else: raise Exception(f"Argument type {type(qarg)} not supported for constrained mixer") grovers_alg(qarg, constraint_oracle, exact = True, winner_state_amount = winner_state_amount(qarg)) def inv_prep_psi(qarg): with invert(): prep_psi(qarg) def constrained_mixer(qarg, beta): with conjugate(inv_prep_psi)(qarg): mcp(beta, qarg, ctrl_state = 0) return constrained_mixer
[docs] def controlled_RX_mixer_gen(predicate): r""" Generate a controlled RX mixer for a given predicate function. Parameters ---------- predicate : function A function receiving a ``QuantumVariable`` and an index $i$. This function returns a ``QuantumBool`` indicating if the predicate is satisfied for ``qv[i]``, that is, if the element ``qv[i]`` should be swapped in. Returns ------- controlled_RX_mixer : function A function receiving a ``QuantumVariable`` and a real parameter $\beta$. This function performs the application of the mixing operator. Examples -------- We define the predicate function for the :ref:`MaxIndepSet <maxIndepSetQAOA>` problem. It returns ``True`` for the index (node) $i$ if all neighbors $j$ of the node $i$ in the graph $G$ are not selected, and ``False`` otherwise. :: from qrisp import QuantumVariable, QuantumBool, h, mcx, auto_uncompute, multi_measurement import networkx as nx G = nx.Graph() G.add_edges_from([(0, 1), (1, 2), (2, 0)]) neighbors_dict = {node: list(G.adj[node]) for node in G.nodes()} def predicate(qv,i): qbl = QuantumBool() if len(neighbors_dict[i])==0: x(qbl) else: mcx([qv[j] for j in neighbors_dict[i]],qbl,ctrl_state='0'*len(neighbors_dict[i])) return qbl qv = QuantumVariable(3) h(qv) qbl = predicate(qv,0) multi_measurement([qv,qbl]) # Yields: {('000', True): 0.125,('100', True): 0.125,('010', False): 0.125,('110', False): 0.125,('001', False): 0.125,('101', False): 0.125,('011', False): 0.125,('111', False): 0.125} The resulting ``controlled_RX_mixer`` then only swaps the node $i$ in if all neighbors $j$ in the graph $G$ are not selected. """ @auto_uncompute def controlled_RX_mixer(qv, beta): m = qv.size for i in range(m): with control(predicate(qv,i)): rx(beta,qv[i]) return controlled_RX_mixer
""" from qrisp import as_hamiltonian @as_hamiltonian def mcp_as_hamiltonian(qv, beta): if qv == "0001": p(beta, qv) elif qv == "0011": p(beta, qv) elif qv == "0111": p(beta, qv) elif qv == "1111": p(beta, qv) """ #formulate on q_array def portfolio_mixer(): """ Multi-Channel constrained mixer to be applied for a discrete portfolio rebalancing problem, as seen in https://arxiv.org/pdf/2006.00354.pdf. This Mixer keeps the constraints in terms of lots on the portfolio intact. This is achieved by mixing between Dicke States. Returns: -------- apply_mixer : function The Mixer to be applied to a QuantumVariable Examples: --------- We initiate a QuantumVariable in the "0011" state and from this partially mix into the Dicke state space with Hamming weight 2. :: from qrisp import QuantumVariable, x import numpy as np qv = QuantumVariable(4) x(qv[2]) x(qv[3]) from qrisp.qaoa.mixers import portfolio_mixer mixer_op = portfolio_mixer() mixer_op(qv, np.pi/8) """ from qrisp.alg_primitives import dicke_state def inv_prepare_dicke(qv, k): with invert(): dicke_state(qv, k) def apply_mixer(q_array, beta): half = int(len(q_array[0])) qv1 = q_array[0] qv2 = q_array[1] #omfg this is harcoded-- problematic one with conjugate(inv_prepare_dicke)(qv1, half): # mehrere mcp-gates, as hamiltonian #mcp_as_hamiltonian(qv1, beta=beta) for i in range(half): ctrl_state = "0" * (half-i-1) + ("1"*(i+1)) #print(ctrl_state) mcp(beta, qv1, ctrl_state = ctrl_state) """ mcp(beta, qv1, ctrl_state = "0001") mcp(beta, qv1, ctrl_state = "0011") mcp(beta, qv1, ctrl_state = "0111") mcp(beta, qv1, ctrl_state = "1111") """ with conjugate(inv_prepare_dicke)(qv2, half): for i in range(half): ctrl_state = "0" * (half-i-1) + ("1"*(i+1)) #print(ctrl_state) mcp(beta, qv2, ctrl_state = ctrl_state) """ mcp(beta, qv2, ctrl_state = "0001") mcp(beta, qv2, ctrl_state = "0011") mcp(beta, qv2, ctrl_state = "0111") mcp(beta, qv2, ctrl_state = "1111") """ return apply_mixer