├── unittests ├── __init__.py ├── test_maxkcut_feasible_initialstate.py ├── test_maxkcut_mixers.py ├── test_maxkcut_one_hot_problem.py └── test_maxkcut_binary_problem.py ├── images ├── E.png ├── interface.png ├── minimal_depth.png └── interface_output.png ├── qaoa ├── __init__.py ├── util │ ├── __init__.py │ ├── post.py │ ├── flip.py │ ├── statistic.py │ └── graphutils.py ├── mixers │ ├── __init__.py │ ├── x_mixer.py │ ├── xy_tensor.py │ ├── grover_mixer.py │ ├── base_mixer.py │ ├── xy_mixer.py │ ├── maxkcut_grover_mixer.py │ └── maxkcut_lx_mixer.py ├── initialstates │ ├── __init__.py │ ├── plus_initialstate.py │ ├── statevector_initialstate.py │ ├── tensor_initialstate.py │ ├── dicke1_2_initialstate.py │ ├── base_initialstate.py │ ├── maxkcut_feasible_initialstate.py │ ├── dicke_initialstate.py │ └── lessthank_initialstate.py └── problems │ ├── __init__.py │ ├── base_problem.py │ ├── exactcover_problem.py │ ├── maxkcut_one_hot_problem.py │ ├── portfolio_problem.py │ ├── qubo_problem.py │ ├── graph_problem.py │ └── maxkcut_binary_powertwo.py ├── CONTRIBUTING.md ├── examples ├── PortfolioOptimization │ ├── data │ │ └── qiskit_finance_seeds.npz │ └── asset_loader.py ├── ExactCover │ ├── data │ │ └── FRCR_6_24_3.txt │ └── tailassignment_loader.py ├── MaxCut │ └── data │ │ ├── er_n10_k4_0.gml │ │ ├── w_ba_n10_k4_0.gml │ │ └── w_ba_n21_k4_0.gml └── plotroutines.py ├── agent ├── valid_initialstates_problems_mixers.txt ├── explainer.py ├── saveembedding.py ├── interface.py ├── planner.py └── coder.py ├── run_graphs.sh ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── setup.py ├── run_all.sh ├── .gitignore ├── run_graphs.py └── README.md /unittests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/HEAD/images/E.png -------------------------------------------------------------------------------- /images/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/HEAD/images/interface.png -------------------------------------------------------------------------------- /images/minimal_depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/HEAD/images/minimal_depth.png -------------------------------------------------------------------------------- /images/interface_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/HEAD/images/interface_output.png -------------------------------------------------------------------------------- /qaoa/__init__.py: -------------------------------------------------------------------------------- 1 | from .qaoa import QAOA 2 | 3 | from . import mixers 4 | from . import problems 5 | from . import initialstates 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | You are more then welcome to contribute. Please fork the repo and create a pull request. License will be the same as of the repo. 2 | -------------------------------------------------------------------------------- /qaoa/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .statistic import Statistic 2 | from .flip import BitFlip 3 | from .post import * 4 | from .graphutils import GraphHandler 5 | -------------------------------------------------------------------------------- /examples/PortfolioOptimization/data/qiskit_finance_seeds.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/HEAD/examples/PortfolioOptimization/data/qiskit_finance_seeds.npz -------------------------------------------------------------------------------- /examples/ExactCover/data/FRCR_6_24_3.txt: -------------------------------------------------------------------------------- 1 | 4:1,3,5,6,8,10,13,14,16,18,20,22 2 | 2:0,2,4,7,9,11,12,15,17,19,21,23 3 | 3:0,2,4,7,8,11,13,15,16,18,20,22 4 | 7:1,3,5,6,9,10,12,14,17,19,21,23 5 | 6:0,2,4,6,8,10,12,14,17,19,21,23 6 | 1:1,3,5,7,9,11,13,15,16,18,20,22 7 | 1,1,0,0,0,0 8 | -------------------------------------------------------------------------------- /qaoa/mixers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_mixer import Mixer 2 | from .xy_mixer import XY 3 | from .x_mixer import X 4 | from .grover_mixer import Grover 5 | from .xy_tensor import XYTensor 6 | from .maxkcut_grover_mixer import MaxKCutGrover 7 | from .maxkcut_lx_mixer import MaxKCutLX 8 | -------------------------------------------------------------------------------- /agent/valid_initialstates_problems_mixers.txt: -------------------------------------------------------------------------------- 1 | The valid initial states are Dicke, Dicke1_2, Plus, LessThanK, StateVector 2 | 3 | The valid mixers are X, XY, Groover, MaxKCutGrover, MaxKCutLX 4 | 5 | The valid problems are ExactCover, GraphProblem, MaxKCutOneHot, MaxKCutBinaryPowerOfTwo, MaxKCutBinaryFullH, PortifolioOptimization -------------------------------------------------------------------------------- /run_graphs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #SBATCH --job-name=run_graphs 3 | # d-hh:mm:ss 4 | #SBATCH --time=30-00:00:00 5 | #SBATCH --output=/home/franzf/MaxKCut/%j.out 6 | #SBATCH --nodes=1 7 | #SBATCH --tasks-per-node=1 8 | #SBATCH --cpus-per-task=1 9 | 10 | python run_graphs.py "$1" "$2" "$3" "$4" "$5" "$6" "$7" 11 | 12 | -------------------------------------------------------------------------------- /qaoa/initialstates/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_initialstate import InitialState 2 | from .plus_initialstate import Plus 3 | from .dicke_initialstate import Dicke 4 | from .statevector_initialstate import StateVector 5 | from .maxkcut_feasible_initialstate import MaxKCutFeasible 6 | from .tensor_initialstate import Tensor 7 | from .lessthank_initialstate import LessThanK 8 | -------------------------------------------------------------------------------- /qaoa/problems/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_problem import Problem 2 | from .qubo_problem import QUBO 3 | from .graph_problem import GraphProblem 4 | from .exactcover_problem import ExactCover 5 | from .portfolio_problem import PortfolioOptimization 6 | from .maxkcut_binary_powertwo import MaxKCutBinaryPowerOfTwo 7 | from .maxkcut_binary_fullH import MaxKCutBinaryFullH 8 | from .maxkcut_one_hot_problem import MaxKCutOneHot 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /qaoa/initialstates/plus_initialstate.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from .base_initialstate import InitialState 5 | 6 | 7 | class Plus(InitialState): 8 | """ 9 | Plus initial state. 10 | 11 | Subclass of `InitialState` class. Creates an the equal superposition of all computational basis states, |+>. 12 | 13 | Methods: 14 | create_circuit(): Generates the quantum circuit creating the plus initial state |+> from the |0> state. 15 | """ 16 | def __init__(self) -> None: 17 | super().__init__() 18 | 19 | def create_circuit(self): 20 | """ 21 | Creates a circuit of Hadamard-gates, which creates the |+> state from the |0> state. 22 | """ 23 | q = QuantumRegister(self.N_qubits) 24 | self.circuit = QuantumCircuit(q) 25 | self.circuit.h(q) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="qaoa", 5 | version="1.2.1", 6 | license="GNU General Public License v3.0", 7 | author="Franz Georg Fuchs", 8 | author_email="franzgeorgfuchs@gmail.com", 9 | description="Quantum Alternating Operator Ansatz/Quantum Approximate Optimization Algorithm (QAOA)", 10 | long_description=open("README.md").read(), 11 | long_description_content_type="text/markdown", 12 | packages=find_packages(exclude=["examples", "images"]), 13 | keywords="quantum computing, qaoa, qiskit", 14 | install_requires=[ 15 | "numpy", 16 | "scipy", 17 | "structlog", 18 | "matplotlib", 19 | "networkx", 20 | "jupyter", 21 | "qiskit==1.1.1", 22 | "qiskit-aer", 23 | "qiskit-algorithms", 24 | "qiskit-finance", 25 | "pylatexenc", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /qaoa/mixers/x_mixer.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from .base_mixer import Mixer 5 | 6 | 7 | class X(Mixer): 8 | """ 9 | X mixer. 10 | 11 | Subclass of the `Mixer` subclass that implements the X mixing operation. 12 | 13 | Attributes: 14 | mixer_param (Parameter): The parameter for the mixer. 15 | N_qubits (int): The number of qubits in the circuit. 16 | circuit (QuantumCircuit): The mixer's quantum circuit. 17 | 18 | Methods: 19 | create_circuit(): Constructs the X mixer circuit. 20 | """ 21 | 22 | def __init__(self) -> None: 23 | """ 24 | Initializes the X mixer. 25 | """ 26 | self.mixer_param = Parameter("x_beta") 27 | 28 | def create_circuit(self): 29 | """ 30 | Constructs the X mixer circuit. 31 | """ 32 | q = QuantumRegister(self.N_qubits) 33 | 34 | self.circuit = QuantumCircuit(q) 35 | self.circuit.rx(-2 * self.mixer_param, range(self.N_qubits)) 36 | -------------------------------------------------------------------------------- /qaoa/initialstates/statevector_initialstate.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from .base_initialstate import InitialState 5 | 6 | 7 | class StateVector(InitialState): 8 | """ 9 | State vector initial state. For custom initial states. 10 | 11 | Subclass of the `InitialState` class. 12 | 13 | Attributes: 14 | statevector (list): The statevector to initialize the circuit with. 15 | 16 | Methods: 17 | create_circuit(): Creates a circuit that creates the initial statevector. 18 | """ 19 | def __init__(self, statevector) -> None: 20 | """ 21 | Args: 22 | statevector (list): The statevector to initialize the circuit with. 23 | """ 24 | super().__init__() 25 | self.statevector = statevector 26 | 27 | def create_circuit(self): 28 | """ 29 | Creates a circuit that makes the initial statevector 30 | """ 31 | q = QuantumRegister(self.N_qubits) 32 | self.circuit = QuantumCircuit(q) 33 | self.circuit.initialize(self.statevector, q) 34 | -------------------------------------------------------------------------------- /run_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | problem_encoding="binary" 4 | 5 | maxdepth=1 6 | shots=100000 7 | 8 | 9 | for casename in {"ErdosRenyi","BarabasiAlbert"} 10 | #for casename in {"Barbell",} 11 | do 12 | 13 | for k in {3,5,6,7} 14 | do 15 | 16 | #case full H 17 | 18 | if [ "$k" -eq 5 ] || [ "$k" -eq 6 ]; then 19 | clf_options=("LessThanK" "max_balanced") 20 | else 21 | clf_options=("LessThanK") 22 | fi 23 | 24 | for clf in "${clf_options[@]}" 25 | do 26 | 27 | for mixer in {"X","Grovertensorized"} 28 | do 29 | 30 | echo "fullH" $k $clf $mixer $casename $maxdepth $shots 31 | bash run_graphs.sh "fullH" $k $clf $mixer $casename $maxdepth $shots 32 | done 33 | done 34 | 35 | #case subspace 36 | 37 | for mixer in {"LX","Grover","Grovertensorized"} 38 | do 39 | 40 | echo "subH" $k "None" $mixer $casename $maxdepth $shots 41 | bash run_graphs.sh "subH" $k "None" $mixer $casename $maxdepth $shots 42 | done 43 | 44 | echo "------" 45 | 46 | done 47 | 48 | 49 | done 50 | 51 | -------------------------------------------------------------------------------- /qaoa/initialstates/tensor_initialstate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from copy import deepcopy 3 | 4 | from qiskit import QuantumRegister, QuantumCircuit 5 | 6 | from .base_initialstate import InitialState 7 | 8 | 9 | class Tensor(InitialState): 10 | """ 11 | Tensor initial state. 12 | 13 | Subclass of the `IntialState` class that creates a tensor out of a circuit. 14 | 15 | Attributions: 16 | subcircuit (InitialState): The circuit that is to be tensorised. 17 | num (int): Number of qubits of the subpart . 18 | 19 | Methods: 20 | create_circuit(): 21 | """ 22 | def __init__(self, subcircuit: InitialState, num: int) -> None: 23 | """ 24 | Args: 25 | subcircuit (InitialState): The circuit that is to be tensorised. 26 | num (int): Number of qubits of the subpart #subN_qubits. 27 | """ 28 | self.num = num 29 | self.subcircuit = subcircuit 30 | self.N_qubits = self.num * self.subcircuit.N_qubits 31 | 32 | def create_circuit(self) -> None: 33 | """ 34 | Creates a circuit that tensorises a given subcircuit. 35 | """ 36 | self.subcircuit.create_circuit() 37 | self.circuit = self.subcircuit.circuit 38 | for v in range(self.num - 1): 39 | self.subcircuit.create_circuit() # self.subcircuit.circuit.qregs) 40 | self.circuit.tensor(self.subcircuit.circuit, inplace=True) 41 | -------------------------------------------------------------------------------- /examples/ExactCover/tailassignment_loader.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os, sys 3 | 4 | def load_FR_CR(filename): 5 | 6 | if not os.path.isfile(filename): 7 | print("File not found.") 8 | raise IOError 9 | 10 | # Matrix in file on format 11 | # C_1: index of ones in row 1 separated by comma 12 | # C_2: index of ones in row 2 ... 13 | # ... 14 | # C_R: index of ones in row R 15 | # best state 16 | 17 | file = np.loadtxt(filename, dtype = str) 18 | 19 | R,F = filename.split('/')[-1].split('_')[1:3] 20 | R,F = int(R), int(F) 21 | 22 | FR = np.zeros((F,R)) 23 | CR = np.zeros(R) 24 | best = np.zeros(R) 25 | 26 | for r in range(R): 27 | CR[r] = float(file[r].split(':')[0]) 28 | indexes = file[r].split(':')[1].split(',') 29 | for ind in indexes: 30 | FR[int(ind), r ] = 1 31 | 32 | best_str = file[R].split(',') 33 | best = np.array([int(i) for i in best_str]) 34 | 35 | return FR, CR, best 36 | 37 | def npy_loader(filename): 38 | """ 39 | Loading the examples saved on npy format. 40 | 41 | Parameters 42 | ---------- 43 | filename : string 44 | filename 45 | 46 | Returns 47 | ------- 48 | FR : array 49 | Constraint matrix 50 | CR : array 51 | Weights 52 | 53 | """ 54 | matrix = np.load(filename) 55 | 56 | # The costs are saved in the last column 57 | CR = matrix[-1] 58 | FR = matrix[:-1] 59 | 60 | return FR, CR 61 | -------------------------------------------------------------------------------- /qaoa/initialstates/dicke1_2_initialstate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumCircuit, QuantumRegister 3 | from qiskit.circuit.library import XXPlusYYGate, PauliEvolutionGate 4 | 5 | from qiskit.quantum_info import SparsePauliOp 6 | 7 | from .base_initialstate import InitialState 8 | 9 | 10 | class Dicke1_2(InitialState): 11 | """ 12 | Dicke1_2 initial state. 13 | 14 | Subclass of the `InitialState` class, and it returns equal superposition Dicke 1 and Dicke 2 states. It is Hard Coded for the case of Hamming weight k = 6. 15 | 16 | Methods: 17 | create_circuit(): Generates the circuit that creates the superposition of Dicke 1 and Dicke 2 states. 18 | """ 19 | 20 | def __init__(self) -> None: 21 | self.k = 6 22 | self.N_qubits = 3 23 | 24 | def create_circuit(self) -> None: 25 | """ 26 | Generates the circuit that creates the superposition of Dicke 1 and Dicke 2 states. 27 | """ 28 | q = QuantumRegister(self.N_qubits) 29 | circuit = QuantumCircuit(q) 30 | X = SparsePauliOp("X") 31 | Y = SparsePauliOp("Y") 32 | operator = Y ^ Y ^ Y 33 | circuit.x(0) 34 | # qc.ry(np.pi/2,2) 35 | circuit.append(PauliEvolutionGate(operator, time=np.pi / 4), q) 36 | circuit.append(XXPlusYYGate(np.arcsin(2 * np.sqrt(2) / 3), np.pi / 2), [0, 1]) 37 | circuit.append(XXPlusYYGate(-np.pi / 2, np.pi / 2), [0, 2]) 38 | circuit.x(1) 39 | circuit.cz(q[1], q[2]) 40 | circuit.x(1) 41 | self.circuit = circuit 42 | -------------------------------------------------------------------------------- /examples/MaxCut/data/er_n10_k4_0.gml: -------------------------------------------------------------------------------- 1 | graph [ 2 | node [ 3 | id 0 4 | label "0" 5 | ] 6 | node [ 7 | id 1 8 | label "1" 9 | ] 10 | node [ 11 | id 2 12 | label "2" 13 | ] 14 | node [ 15 | id 3 16 | label "3" 17 | ] 18 | node [ 19 | id 4 20 | label "4" 21 | ] 22 | node [ 23 | id 5 24 | label "5" 25 | ] 26 | node [ 27 | id 6 28 | label "6" 29 | ] 30 | node [ 31 | id 7 32 | label "7" 33 | ] 34 | node [ 35 | id 8 36 | label "8" 37 | ] 38 | node [ 39 | id 9 40 | label "9" 41 | ] 42 | edge [ 43 | source 0 44 | target 1 45 | weight 1 46 | ] 47 | edge [ 48 | source 0 49 | target 3 50 | weight 1 51 | ] 52 | edge [ 53 | source 0 54 | target 9 55 | weight 1 56 | ] 57 | edge [ 58 | source 1 59 | target 4 60 | weight 1 61 | ] 62 | edge [ 63 | source 1 64 | target 5 65 | weight 1 66 | ] 67 | edge [ 68 | source 1 69 | target 6 70 | weight 1 71 | ] 72 | edge [ 73 | source 2 74 | target 5 75 | weight 1 76 | ] 77 | edge [ 78 | source 2 79 | target 7 80 | weight 1 81 | ] 82 | edge [ 83 | source 2 84 | target 9 85 | weight 1 86 | ] 87 | edge [ 88 | source 3 89 | target 7 90 | weight 1 91 | ] 92 | edge [ 93 | source 3 94 | target 8 95 | weight 1 96 | ] 97 | edge [ 98 | source 3 99 | target 9 100 | weight 1 101 | ] 102 | edge [ 103 | source 4 104 | target 7 105 | weight 1 106 | ] 107 | edge [ 108 | source 5 109 | target 6 110 | weight 1 111 | ] 112 | edge [ 113 | source 5 114 | target 8 115 | weight 1 116 | ] 117 | edge [ 118 | source 7 119 | target 9 120 | weight 1 121 | ] 122 | ] 123 | -------------------------------------------------------------------------------- /qaoa/mixers/xy_tensor.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | from qiskit.circuit.library import XXPlusYYGate 4 | 5 | from .base_mixer import Mixer 6 | from .xy_mixer import XY 7 | from qaoa.initialstates.tensor_initialstate import Tensor 8 | 9 | 10 | class XYTensor(Mixer): 11 | """ 12 | XY tensor mixer for the Max k-Cut problem. 13 | 14 | Subclass of the `Mixer` class that implements the XY tensor mixing operation for the Max k-Cut problem. 15 | 16 | Attributes: 17 | k_cuts (int): The number of cuts in the Max k-Cut problem. 18 | topology (list): The topology of the mixer, default is None. 19 | num_V (int): The number of vertices in the Max k-Cut problem. 20 | 21 | Methods: 22 | create_circuit(): Constructs the XY tensor mixer circuit for the Max k-Cut problem. 23 | """ 24 | 25 | def __init__(self, k_cuts: int, topology=None) -> None: 26 | """ 27 | Initializes the XYTensor mixer for the Max k-Cut problem. 28 | 29 | Args: 30 | k_cuts (int): The number of cuts in the Max k-Cut problem. 31 | topology (list, optional): The topology of the mixer. If None, defaults to "ring" topology. 32 | """ 33 | self.k_cuts = k_cuts 34 | self.topology = topology 35 | 36 | def create_circuit(self) -> None: 37 | """ 38 | Constructs the XY tensor mixer circuit for the Max k-Cut problem. 39 | 40 | Raises: 41 | ValueError: If the total number of qubits is not a multiple of `k_cuts`. 42 | """ 43 | self.num_V = self.N_qubits / self.k_cuts 44 | 45 | if not self.num_V.is_integer(): 46 | raise ValueError( 47 | "Total qubits=" 48 | + str(self.N_qubits) 49 | + " is not a multiple of " 50 | + str(self.k_cuts) 51 | ) 52 | self.num_V = int(self.num_V) 53 | 54 | xy = XY(self.topology) 55 | xy.setNumQubits(self.k_cuts) 56 | 57 | self.tensor = Tensor(xy, self.num_V) 58 | 59 | self.tensor.create_circuit() 60 | self.circuit = self.tensor.circuit 61 | -------------------------------------------------------------------------------- /qaoa/mixers/grover_mixer.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | from qiskit.circuit.library import PhaseGate 3 | 4 | from .base_mixer import Mixer 5 | from qaoa.initialstates.base_initialstate import InitialState 6 | 7 | 8 | class Grover(Mixer): 9 | """ 10 | Grover mixer. 11 | 12 | Subclass of the `Mixer` subclass that implements the Grover mixing operation. 13 | 14 | Attributes: 15 | subcircuit (InitialState): The initial state circuit. 16 | circuit (QuantumCircuit): The quantum circuit representing the mixer. 17 | mixer_param (Parameter): The parameter for the mixer. 18 | N_qubits (int): The number of qubits in the mixer circuit. 19 | 20 | Methods: 21 | create_circuit(): Constructs the Grover mixer circuit using the subcircuit. 22 | """ 23 | 24 | def __init__(self, subcircuit: InitialState) -> None: 25 | """ 26 | Initializes the Grover mixer. 27 | 28 | Args: 29 | subcircuit (InitialState): The initial state circuit. 30 | """ 31 | self.subcircuit = subcircuit 32 | self.mixer_param = Parameter("x_beta") 33 | 34 | def create_circuit(self): 35 | """ 36 | Constructs the Grover mixer circuit using the subcircuit. 37 | 38 | Given feasible states f \in F, 39 | and let US be the circuit that prepares US = 1/|F| \sum_{f\inF} |f>. 40 | The Grover mixer has the form US^\dagger X^n C^{n-1}Phase X^n US. 41 | """ 42 | 43 | self.subcircuit.setNumQubits(self.N_qubits) 44 | self.subcircuit.create_circuit() 45 | US = self.subcircuit.circuit 46 | 47 | # US^\dagger 48 | self.circuit = US.inverse() 49 | # X^n 50 | self.circuit.x(range(self.subcircuit.N_qubits)) 51 | # C^{n-1}Phase 52 | if self.subcircuit.N_qubits == 1: 53 | phase_gate = PhaseGate(-self.mixer_param) 54 | else: 55 | phase_gate = PhaseGate(-self.mixer_param).control( 56 | self.subcircuit.N_qubits - 1 57 | ) 58 | self.circuit.append(phase_gate, self.circuit.qubits) 59 | # X^n 60 | self.circuit.x(range(self.subcircuit.N_qubits)) 61 | # US 62 | self.circuit.compose(US, range(self.subcircuit.N_qubits), inplace=True) 63 | -------------------------------------------------------------------------------- /qaoa/initialstates/base_initialstate.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseInitialState(ABC): 5 | """ 6 | Base class for defining initial quantum states. 7 | 8 | This is an abstract base class (ABC) that defines the basic structure of 9 | initial quantum states. Subclasses must implement the `create_circuit` 10 | method to create a quantum circuit for the specific initial state. 11 | 12 | Attributes: 13 | circuit (QuantumCircuit): The quantum circuit representing the initial state. 14 | N_qubits (int): The number of qubits in the quantum circuit. 15 | """ 16 | 17 | def __init__(self) -> None: 18 | """ 19 | Initializes a BaseInitialState object. 20 | 21 | The `circuit` attribute is set to None initially, and `N_qubits` 22 | is not defined until `setNumQubits` is called. 23 | """ 24 | self.circuit = None 25 | 26 | def setNumQubits(self, n): 27 | """ 28 | Set the number of qubits for the quantum circuit. 29 | 30 | Args: 31 | n (int): The number of qubits to set. 32 | """ 33 | self.N_qubits = n 34 | 35 | 36 | class InitialState(BaseInitialState): 37 | """ 38 | Abstract subclass for defining specific initial quantum states. 39 | 40 | This abstract subclass of `BaseInitialState` is meant for defining 41 | concrete initial quantum states. Subclasses of `InitialState` must 42 | implement the `create_circuit` method to create a quantum circuit 43 | representing the specific initial state. 44 | 45 | Note: 46 | Subclasses of `InitialState` must provide an implementation 47 | for the `create_circuit` method. 48 | 49 | Example: 50 | ```python 51 | class MyInitialState(InitialState): 52 | def create_circuit(self): 53 | # Define the quantum circuit for a custom initial state. 54 | ... 55 | ``` 56 | """ 57 | 58 | @abstractmethod 59 | def create_circuit(self): 60 | """ 61 | Abstract method to create the quantum circuit for the initial state. 62 | 63 | Subclasses must implement this method to define the quantum circuit 64 | for the specific initial state they represent. 65 | 66 | Raises: 67 | NotImplementedError: This method must be implemented by subclasses. 68 | """ 69 | pass 70 | -------------------------------------------------------------------------------- /examples/PortfolioOptimization/asset_loader.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import datetime 4 | 5 | from qiskit_finance.data_providers import RandomDataProvider 6 | 7 | 8 | class AssetData: 9 | def __init__( 10 | self, N_assets, num_days=101, seed=0, start_time=datetime.datetime(2020, 1, 1) 11 | ): 12 | """ 13 | init function that initializes member variables 14 | 15 | :param params: additional parameters 16 | """ 17 | self.N = N_assets 18 | self.num_days = num_days 19 | self.start_time = start_time 20 | self.end_time = start_time + datetime.timedelta(self.num_days) 21 | 22 | self.tickers = [("TICKER%s" % i) for i in range(self.N)] 23 | self.fin_data = RandomDataProvider( 24 | tickers=self.tickers, start=self.start_time, end=self.end_time, seed=seed 25 | ) 26 | self.fin_data.run() 27 | 28 | self.cov_matrix = self.fin_data.get_period_return_covariance_matrix() 29 | self.exp_return = self.fin_data.get_period_return_mean_vector() 30 | 31 | def plotAssets(self, figsize=(12, 4)): 32 | fig = plt.figure(figsize=figsize) 33 | gs = fig.add_gridspec(1, 3) 34 | axs = [None] * 2 35 | axs[0] = fig.add_subplot(gs[0, 0:2]) 36 | t = [self.start_time + datetime.timedelta(dt) for dt in range(self.num_days)] 37 | for i, ticker in enumerate(self.tickers): 38 | axs[0].plot(t, self.fin_data._data[i], label=ticker) 39 | axs[0].set_xticklabels(axs[0].get_xticklabels(), rotation=-30) 40 | axs[0].legend() 41 | axs[0].set_title("time development") 42 | 43 | axs[1] = fig.add_subplot(gs[0, 2]) 44 | im = axs[1].imshow(self.cov_matrix) 45 | fig.colorbar(im, ax=axs[1], shrink=0.8) 46 | axs[1].set_title("Period return cov. matrix") 47 | 48 | def plotPeriodReturns(self, figsize=(8, 4)): 49 | fig = plt.figure(figsize=figsize) 50 | gs = fig.add_gridspec(1, 3) 51 | axs = [None] * 2 52 | axs[0] = fig.add_subplot(gs[0, 0:2]) 53 | t = [self.start_time + datetime.timedelta(dt) for dt in range(self.num_days)] 54 | for i, ticker in enumerate(self.tickers): 55 | axs[0].plot(t, self.fin_data._data[i], label=ticker) 56 | axs[0].set_xticklabels(axs[0].get_xticklabels(), rotation=-30) 57 | axs[0].legend() 58 | axs[0].set_title("time development") 59 | -------------------------------------------------------------------------------- /qaoa/mixers/base_mixer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class MixerBase(ABC): 5 | """ 6 | Base class for defining quantum mixing operations. 7 | 8 | This is an abstract base class (ABC) that provides a common interface for 9 | quantum mixing operations. Subclasses can inherit from this class to define 10 | specific mixing operations. 11 | 12 | Attributes: 13 | circuit (QuantumCircuit): The quantum circuit associated with the mixer. 14 | N_qubits (int): The number of qubits in the mixer circuit. 15 | """ 16 | 17 | def __init__(self) -> None: 18 | """ 19 | Initializes a MixerBase object. 20 | 21 | The `circuit` attribute is set to None initially, and `N_qubits` 22 | is not defined until `setNumQubits` is called. 23 | """ 24 | self.circuit = None 25 | 26 | def setNumQubits(self, n): 27 | """ 28 | Set the number of qubits for the quantum mixer circuit. 29 | 30 | Args: 31 | n (int): The number of qubits to set. 32 | """ 33 | self.N_qubits = n 34 | 35 | 36 | class Mixer(MixerBase): 37 | """ 38 | Abstract subclass for defining specific quantum mixing operations. 39 | 40 | This abstract subclass of `MixerBase` is meant for defining concrete quantum 41 | mixing operations. Subclasses of `Mixer` must implement the `create_circuit` 42 | method to create the associated quantum circuit for mixing. 43 | 44 | Attributes: 45 | circuit (QuantumCircuit): The quantum circuit associated with the mixer. 46 | 47 | Methods: 48 | create_circuit(): Abstract method to create the quantum circuit 49 | representing the mixing operation. 50 | 51 | Note: 52 | Subclasses of `Mixer` must provide an implementation for the `create_circuit` 53 | method. 54 | 55 | Example: 56 | ```python 57 | class MyMixer(Mixer): 58 | def create_circuit(self): 59 | # Define the quantum circuit for the custom mixing operation. 60 | ... 61 | ``` 62 | """ 63 | 64 | @abstractmethod 65 | def create_circuit(self): 66 | """ 67 | Abstract method to create the quantum circuit representing the mixing operation. 68 | 69 | Subclasses must implement this method to define the quantum circuit 70 | that represents the specific mixing operation. 71 | 72 | Returns: 73 | QuantumCircuit: The quantum circuit representing the mixing operation. 74 | """ 75 | pass 76 | -------------------------------------------------------------------------------- /unittests/test_maxkcut_feasible_initialstate.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import networkx as nx 4 | import numpy as np 5 | 6 | from qiskit_aer import Aer 7 | from qiskit.quantum_info import Statevector 8 | 9 | sys.path.append("../") 10 | 11 | from qaoa.initialstates import MaxKCutFeasible 12 | 13 | 14 | class TestMaxKCutFeasibleInitialstate(unittest.TestCase): 15 | def __init__(self, methodname): 16 | super().__init__(methodname) 17 | 18 | V = np.arange(0, 1, 1) 19 | E = [] 20 | 21 | self.G = nx.Graph() 22 | self.G.add_nodes_from(V) 23 | self.G.add_weighted_edges_from(E) 24 | 25 | def test_feasible_initialstate_binary(self): 26 | """ 27 | Test that MaxKCutFeasible (case: binary) prepares the correct initialstate 28 | for all k in [3, 5, 6, 7] 29 | """ 30 | coen = ["LessThanK", "Dicke1_2"] 31 | for color_encoding in coen: 32 | for k in [3, 5, 6, 7]: 33 | if (color_encoding == "Dicke1_2") and (k != 6): 34 | continue 35 | 36 | k_bits = int(np.ceil(np.log2(k))) 37 | initialstate = MaxKCutFeasible( 38 | k, "binary", color_encoding=color_encoding 39 | ) 40 | initialstate.setNumQubits(k_bits) 41 | initialstate.create_circuit() 42 | circuit = initialstate.circuit 43 | 44 | statevector = Statevector(circuit) 45 | sample_counts = statevector.sample_counts(shots=100000) 46 | for string in sample_counts: 47 | string = string[::-1] 48 | self.assertTrue(string not in initialstate.infeasible) 49 | 50 | def test_feasible_initialstate_onehot(self): 51 | """ 52 | Test that MaxKCutFeasible (case: onehot) prepares the correct initialstate 53 | for all 2 <= k <= 8. 54 | """ 55 | for k in range(2, 9): 56 | initialstate = MaxKCutFeasible(k, "onehot") 57 | initialstate.setNumQubits(k) 58 | initialstate.create_circuit() 59 | circuit = initialstate.circuit 60 | 61 | computed = Statevector(circuit) 62 | expected = np.zeros(2**k) 63 | for i in range(k): 64 | expected[2**i] = 1 / np.sqrt(k) 65 | equiv = np.allclose(computed, expected) 66 | msg = f"One-Hot: k = {k}. Expected: {expected}, computed: {computed}." 67 | self.assertTrue(equiv, msg) 68 | 69 | 70 | if __name__ == "__main__": 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /unittests/test_maxkcut_mixers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import networkx as nx 4 | import numpy as np 5 | 6 | from qiskit.quantum_info import Statevector 7 | from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister 8 | 9 | sys.path.append("../") 10 | 11 | from qaoa.mixers import MaxKCutGrover, MaxKCutLX 12 | from qaoa.initialstates import MaxKCutFeasible 13 | 14 | 15 | class TestFeasibleOutputsFromMixers(unittest.TestCase): 16 | def __init__(self, methodname): 17 | super().__init__(methodname) 18 | 19 | V = np.arange(0, 1, 1) 20 | E = [] 21 | 22 | self.G = nx.Graph() 23 | self.G.add_nodes_from(V) 24 | self.G.add_weighted_edges_from(E) 25 | 26 | def test_LXmixer_binary(self): 27 | for mixertype in ["LX", "Grover"]: 28 | coen = ["LessThanK", "Dicke1_2"] 29 | for color_encoding in coen: 30 | for k in [3, 5, 6, 7]: 31 | if (color_encoding == "Dicke1_2") and (k != 6): 32 | continue 33 | 34 | k_bits = int(np.ceil(np.log2(k))) 35 | initialstate = MaxKCutFeasible( 36 | k, "binary", color_encoding=color_encoding 37 | ) 38 | initialstate.setNumQubits(k_bits) 39 | initialstate.create_circuit() 40 | 41 | if mixertype == "LX": 42 | mixer = MaxKCutLX(k, color_encoding=color_encoding) 43 | else: 44 | mixer = MaxKCutGrover( 45 | k, 46 | color_encoding=color_encoding, 47 | problem_encoding="binary", 48 | tensorized=False, 49 | ) 50 | mixer.setNumQubits(k_bits) 51 | mixer.create_circuit() 52 | 53 | circuit = initialstate.circuit 54 | circuit.compose(mixer.circuit, inplace=True) 55 | 56 | circuit = circuit.assign_parameters( 57 | {circuit.parameters[0]: 0.5912847}, 58 | inplace=False, 59 | ) 60 | 61 | statevector = Statevector(circuit) 62 | sample_counts = statevector.sample_counts(shots=100000) 63 | for string in sample_counts: 64 | string = string[::-1] 65 | self.assertTrue(string not in initialstate.infeasible) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | #vim 132 | *~ 133 | *.swp 134 | 135 | *.yaml 136 | *.sh 137 | 138 | # Embedding for agent 139 | embeddings/ 140 | 141 | *.pickle 142 | -------------------------------------------------------------------------------- /qaoa/util/post.py: -------------------------------------------------------------------------------- 1 | import statistics as stat 2 | import numpy as np 3 | 4 | 5 | def post_processing(instance, samples, K=5): 6 | """Performs classical post-processing on bitstrings by applying random bit flips. 7 | Resets and updates `self.stat`. 8 | 9 | Args: 10 | Instance (object): Object with the following attributes 11 | - problem (*Problem*) 12 | - flipper (*BitFlip*) 13 | - stat (*Statistic*) 14 | samples (dict or list or str): The bitstring(s) to be processed. 15 | K (int): The number of times to iterate through each bitstring and apply random bit flips. 16 | 17 | Returns: 18 | dict: A dictionary with the altered bitstrings as keys and their counts as values. 19 | If no better bitstring is found, the original bitstring is the key. 20 | """ 21 | instance.stat.reset() 22 | hist_post = {} 23 | 24 | if isinstance(samples, str): 25 | samples = [samples] 26 | 27 | for string in samples: 28 | boosted = instance.flipper.boost_samples( 29 | problem=instance.problem, string=string, K=K 30 | ) 31 | try: 32 | count = samples[string] 33 | except: 34 | count = 1 35 | 36 | instance.stat.add_sample( 37 | instance.problem.cost(boosted[::-1]), count, boosted[::-1] 38 | ) 39 | hist_post[boosted] = hist_post.get(boosted, 0) + count 40 | return hist_post 41 | 42 | 43 | def post_process_all_depths(instance, K=5): 44 | """Performs post-processing of `job.result().get_counts()` 100 times after each layer. 45 | 46 | Args: 47 | instance (object): Object with the following attributes 48 | - samplecount_hists (*dict*): A dictionary where keys are layer depths and values are histograms of sample counts. 49 | - stat (*Statistic*) 50 | 51 | Returns: 52 | tuple: A tuple containing 53 | - *np.ndarray*: Means of expectation values after post-processing for each layer. 54 | - *np.ndarray*: Variances of expectation values after post-processing for each layer. 55 | """ 56 | exp_in_layers = {} 57 | exp = [] 58 | var = [] 59 | for d, hist in instance.samplecount_hists.items(): 60 | if not isinstance(hist, dict): 61 | raise TypeError 62 | for i in range(100): 63 | post_processing( 64 | instance=instance, 65 | samples=hist, 66 | K=K, 67 | ) 68 | exp_in_layers[d] = exp_in_layers.get(d, []) + [-instance.stat.get_CVaR()] 69 | exp.append(stat.mean(exp_in_layers[d])) 70 | var.append(stat.variance(exp_in_layers[d])) 71 | return (np.array(exp), np.array(var)) 72 | -------------------------------------------------------------------------------- /qaoa/mixers/xy_mixer.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | from qiskit.circuit.library import XXPlusYYGate 4 | 5 | from .base_mixer import Mixer 6 | 7 | import math 8 | import itertools 9 | 10 | import numpy as np 11 | 12 | 13 | class XY(Mixer): 14 | """ 15 | XY mixer. 16 | 17 | Subclass of the `Mixer` subclass that implements the XY mixing operation. 18 | 19 | Attributes: 20 | topology (list): The topology of the mixer, default is None. 21 | mixer_param (Parameter): The parameter for the XY mixer. 22 | N_qubits (int): The number of qubits in the mixer circuit. 23 | circuit (QuantumCircuit): The quantum circuit representing the XY mixer. 24 | 25 | Methods: 26 | create_circuit(): Constructs the XY mixer circuit using the specified topology. 27 | generate_pairs(n, case="ring"): Generates pairs of qubits based on the specified topology. 28 | """ 29 | 30 | def __init__(self, topology=None, case="ring") -> None: 31 | """ 32 | Initializes the XY mixer. 33 | 34 | Args: 35 | topology (list, optional): The topology of the mixer as list of connected pairs. If None, generated by the 'case' parameter 36 | case (str, optional): The topology as string. If topology is None, case is used to generate topology using "ring" as default 37 | """ 38 | self.topology = topology 39 | self.case = case 40 | self.mixer_param = Parameter("x_beta") 41 | 42 | def create_circuit(self): 43 | """ 44 | Constructs the XY mixer circuit using the specified topology. 45 | 46 | If no topology is specified, it defaults to a "ring" topology. 47 | """ 48 | if not self.topology: 49 | self.topology = XY.generate_pairs(self.N_qubits, case=self.case) 50 | 51 | q = QuantumRegister(self.N_qubits) 52 | self.circuit = QuantumCircuit(q) 53 | 54 | for i, e in enumerate(self.topology): 55 | self.circuit.append(XXPlusYYGate(0.5 * self.mixer_param), e) 56 | 57 | @staticmethod 58 | def generate_pairs(n, case="ring"): 59 | """_summary_ 60 | 61 | Args: 62 | n (int): The number of qubits. 63 | case (str, optional): Topology. Defaults to "ring". 64 | 65 | Returns: 66 | list: A list of pairs of qubit indices based on the specified topology. 67 | """ 68 | assert(case in ["ring", "chain"]) 69 | 70 | # default ring, otherwise "chain" 71 | if n < 2: 72 | return [] # Not enough elements to form any pairs 73 | 74 | pairs = [[i, i + 1] for i in range(n - 1)] 75 | 76 | if case == "ring": 77 | pairs.append([n - 1, 0]) 78 | 79 | return pairs 80 | -------------------------------------------------------------------------------- /examples/MaxCut/data/w_ba_n10_k4_0.gml: -------------------------------------------------------------------------------- 1 | graph [ 2 | node [ 3 | id 0 4 | label "0" 5 | ] 6 | node [ 7 | id 1 8 | label "1" 9 | ] 10 | node [ 11 | id 2 12 | label "2" 13 | ] 14 | node [ 15 | id 3 16 | label "3" 17 | ] 18 | node [ 19 | id 4 20 | label "4" 21 | ] 22 | node [ 23 | id 5 24 | label "5" 25 | ] 26 | node [ 27 | id 6 28 | label "6" 29 | ] 30 | node [ 31 | id 7 32 | label "7" 33 | ] 34 | node [ 35 | id 8 36 | label "8" 37 | ] 38 | node [ 39 | id 9 40 | label "9" 41 | ] 42 | edge [ 43 | source 0 44 | target 4 45 | weight 0.3246074330296992 46 | ] 47 | edge [ 48 | source 0 49 | target 7 50 | weight 0.6719596645247027 51 | ] 52 | edge [ 53 | source 1 54 | target 4 55 | weight 0.5033779645445525 56 | ] 57 | edge [ 58 | source 1 59 | target 5 60 | weight 0.8197417437657258 61 | ] 62 | edge [ 63 | source 1 64 | target 6 65 | weight 0.1689752608979167 66 | ] 67 | edge [ 68 | source 2 69 | target 4 70 | weight 0.8578794331926194 71 | ] 72 | edge [ 73 | source 2 74 | target 5 75 | weight 0.10889087475274517 76 | ] 77 | edge [ 78 | source 2 79 | target 6 80 | weight 0.29609241287667165 81 | ] 82 | edge [ 83 | source 2 84 | target 7 85 | weight 0.3385595778596342 86 | ] 87 | edge [ 88 | source 2 89 | target 9 90 | weight 0.49871018015134483 91 | ] 92 | edge [ 93 | source 3 94 | target 4 95 | weight 0.5646214337732219 96 | ] 97 | edge [ 98 | source 3 99 | target 5 100 | weight 0.22675259631935551 101 | ] 102 | edge [ 103 | source 3 104 | target 6 105 | weight 0.42653644275637315 106 | ] 107 | edge [ 108 | source 3 109 | target 8 110 | weight 0.9458888986056379 111 | ] 112 | edge [ 113 | source 4 114 | target 5 115 | weight 0.6274516118216547 116 | ] 117 | edge [ 118 | source 4 119 | target 7 120 | weight 0.6461631361850252 121 | ] 122 | edge [ 123 | source 4 124 | target 8 125 | weight 0.07077280704236999 126 | ] 127 | edge [ 128 | source 4 129 | target 9 130 | weight 0.061962597519110374 131 | ] 132 | edge [ 133 | source 5 134 | target 6 135 | weight 0.12115603714424517 136 | ] 137 | edge [ 138 | source 5 139 | target 7 140 | weight 0.6596288196387271 141 | ] 142 | edge [ 143 | source 5 144 | target 8 145 | weight 0.8184188538157214 146 | ] 147 | edge [ 148 | source 5 149 | target 9 150 | weight 0.686461546892179 151 | ] 152 | edge [ 153 | source 7 154 | target 8 155 | weight 0.5014423218237379 156 | ] 157 | edge [ 158 | source 8 159 | target 9 160 | weight 0.11336603624375363 161 | ] 162 | ] 163 | -------------------------------------------------------------------------------- /qaoa/util/flip.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumCircuit, QuantumRegister 3 | 4 | 5 | class BitFlip: 6 | """ 7 | BitFlip class for performing random bit flips on a string to increase cost. 8 | 9 | Attributes: 10 | circuit (QuantumCircuit): Quantum circuit for bit flips. 11 | N_qubits (int): Number of qubits in the circuit. 12 | 13 | """ 14 | 15 | def __init__(self, n): 16 | """ 17 | Initializes the BitFlip class. 18 | 19 | Args: 20 | n (int): Number of qubits in the circuit. 21 | """ 22 | self.circuit = None 23 | self.N_qubits = n 24 | 25 | def boost_samples(self, problem, string, K=5): 26 | """ 27 | Random bitflips on string/list of strings to increase cost. 28 | 29 | Args: 30 | problem: BaseType Problem. 31 | string (str): String or list of strings. 32 | K (int): Number of iteratations through string while flipping. 33 | 34 | Returns: 35 | str: string after bitflips. 36 | """ 37 | string_arr = np.array([int(bit) for bit in string]) 38 | old_string = string 39 | cost = problem.cost(string[::-1]) 40 | 41 | for _ in range(K): 42 | shuffled_indices = np.arange(self.N_qubits) 43 | np.random.shuffle(shuffled_indices) 44 | 45 | for i in shuffled_indices: 46 | string_arr_altered = np.copy(string_arr) 47 | string_arr_altered[i] = not (string_arr[i]) 48 | string_altered = "".join(map(str, string_arr_altered)) 49 | new_cost = problem.cost(string_altered[::-1]) 50 | 51 | if new_cost > cost: 52 | cost = new_cost 53 | string_arr = string_arr_altered 54 | string = string_altered 55 | 56 | return string 57 | 58 | def xor(self, old_string, new_string): 59 | """ 60 | Finds (old_string XOR new_string). 61 | 62 | Args: 63 | old_string (str): string before bitflips 64 | new_string (str): string after bitflips 65 | 66 | Returns: 67 | list: Qubits on which to apply X-gate 68 | if 1 at pos n - i, apply X-gate to qubit i 69 | if 0 at pos n - j, do nothing to qubit j 70 | """ 71 | old = np.array([int(bit) for bit in old_string]) 72 | new = np.array([int(bit) for bit in new_string]) 73 | xor = [] 74 | 75 | for a, b in zip(old, new): 76 | xor.append((a and (not b)) or ((not a) and b)) 77 | 78 | return xor 79 | 80 | def create_circuit(self, xor: list[int | bool]) -> None: 81 | """ 82 | Creates quantum circuit that performs bitflips. 83 | 84 | Args: 85 | xor (list): list of qubits on which to apply X-gate. 86 | - If 1 at pos n - i, apply X-gate to qubit i 87 | - If 0 at pos n - j, do nothing to qubit j 88 | """ 89 | q = QuantumRegister(self.N_qubits) 90 | self.circuit = QuantumCircuit(q) 91 | indices_flip = np.where(xor[::-1])[0] 92 | if np.any(indices_flip): 93 | self.circuit.x(indices_flip) 94 | -------------------------------------------------------------------------------- /qaoa/problems/base_problem.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseProblem(ABC): 5 | """ 6 | Base class for defining optimization problems. 7 | 8 | This is an abstract base class (ABC) that provides a common interface for 9 | optimization problems. Subclasses can inherit from this class to define 10 | specific optimization problems. 11 | 12 | Attributes: 13 | circuit (QuantumCircuit): The quantum circuit associated with the problem. 14 | """ 15 | 16 | def __init__(self) -> None: 17 | """ 18 | Initializes a BaseProblem object. 19 | 20 | The `circuit` attribute is set to None initially and can be 21 | assigned a quantum circuit later. 22 | """ 23 | self.circuit = None 24 | self.N_ancilla_qubits = 0 25 | 26 | 27 | class Problem(BaseProblem): 28 | """ 29 | Abstract subclass for defining specific optimization problems. 30 | 31 | This abstract subclass of `BaseProblem` is meant for defining concrete 32 | optimization problems. Subclasses of `Problem` must implement the `cost` 33 | and `create_circuit` methods to define the problem's cost function and 34 | create the associated quantum circuit. 35 | 36 | Attributes: 37 | circuit (QuantumCircuit): The quantum circuit associated with the problem. 38 | 39 | Methods: 40 | cost(string): Abstract method to calculate the cost of a solution. 41 | create_circuit(): Abstract method to create the quantum circuit 42 | representing the problem. 43 | isFeasible(string): Checks if a given solution string is feasible. 44 | This method returns True by default and can be overridden by 45 | subclasses to implement custom feasibility checks. 46 | 47 | Note: 48 | Subclasses of `Problem` must provide implementations for the `cost` 49 | and `create_circuit` methods. 50 | 51 | Example: 52 | ```python 53 | class MyProblem(Problem): 54 | def cost(self, string): 55 | # Define the cost calculation for the optimization problem. 56 | ... 57 | 58 | def create_circuit(self): 59 | # Define the quantum circuit for the optimization problem. 60 | ... 61 | ``` 62 | """ 63 | 64 | @abstractmethod 65 | def cost(self, string): 66 | """ 67 | Abstract method to calculate the cost of a solution. 68 | 69 | Subclasses must implement this method to define how the cost of a 70 | solution is calculated for the specific optimization problem. 71 | 72 | Args: 73 | string (str): A solution string or configuration to evaluate. 74 | 75 | Returns: 76 | float: The cost of the given solution. 77 | """ 78 | pass 79 | 80 | @abstractmethod 81 | def create_circuit(self): 82 | """ 83 | Abstract method to create the quantum circuit representing the problem. 84 | 85 | Subclasses must implement this method to define the quantum circuit 86 | that represents the optimization problem. 87 | 88 | Returns: 89 | QuantumCircuit: The quantum circuit representing the problem. 90 | """ 91 | pass 92 | 93 | def isFeasible(self, string): 94 | """ 95 | Check if a solution string is feasible. 96 | 97 | This method provides a default implementation that always returns True. 98 | Subclasses can override this method to implement custom feasibility checks. 99 | 100 | Args: 101 | string (str): A solution string or configuration to check. 102 | 103 | Returns: 104 | bool: True if the solution is feasible; otherwise, False. 105 | """ 106 | return True 107 | 108 | def computeMinMaxCosts(self): 109 | """ 110 | Brute force method to compute min and max cost of feasible solution 111 | """ 112 | import itertools 113 | 114 | max_cost = float("-inf") 115 | min_cost = float("inf") 116 | for s in ["".join(i) for i in itertools.product("01", repeat=self.N_qubits)]: 117 | if self.isFeasible(s): 118 | cost = -self.cost(s) 119 | max_cost = max(max_cost, cost) 120 | min_cost = min(min_cost, cost) 121 | return min_cost, max_cost 122 | -------------------------------------------------------------------------------- /run_graphs.py: -------------------------------------------------------------------------------- 1 | import math 2 | import pickle 3 | import sys 4 | import os 5 | 6 | import numpy as np 7 | import networkx as nx 8 | import matplotlib.pyplot as plt 9 | 10 | from qaoa import QAOA, mixers, initialstates # type: ignore 11 | from qaoa.initialstates import MaxKCutFeasible 12 | from qaoa.mixers import MaxKCutGrover, MaxKCutLX, XYTensor 13 | from qaoa.problems import MaxKCutBinaryPowerOfTwo, MaxKCutBinaryFullH 14 | 15 | from qiskit_algorithms.optimizers import SPSA, COBYLA, ADAM, NFT, NELDER_MEAD 16 | 17 | from qiskit_aer import AerSimulator 18 | 19 | 20 | def main( 21 | method, 22 | k, 23 | clf, 24 | mixerstr, 25 | casename, 26 | maxdepth, 27 | shots, 28 | ): 29 | 30 | angles = {"gamma": [0, 2 * np.pi, 20], "beta": [0, 2 * np.pi, 20]} 31 | optimizer = [COBYLA, {"maxiter": 100, "tol": 1e-3, "rhobeg": 0.05}] 32 | problem_encoding = "binary" 33 | 34 | if casename == "Barbell": 35 | V = np.arange(0, 2, 1) 36 | E = [(0, 1, 1.0)] 37 | G = nx.Graph() 38 | G.add_nodes_from(V) 39 | G.add_weighted_edges_from(E) 40 | elif casename == "BarabasiAlbert": 41 | G = nx.read_gml("data/w_ba_n10_k4_0.gml") 42 | # max_val = np.array([8.657714089848158, 10.87975400338161, 11.059417685176726, 11.059417685176726, 11.059417685176726, 11.059417685176726, 11.059417685176726]) 43 | elif casename == "ErdosRenyi": 44 | G = nx.read_gml("data/er_n10_k4_0.gml") 45 | # max_val = np.array([12, 16, 16, 16, 16, 16, 16]) 46 | 47 | string_identifier = ( 48 | "method" 49 | + str(method) 50 | + "_" 51 | + "k" 52 | + str(k) 53 | + "_" 54 | + "clf" 55 | + str(clf) 56 | + "_" 57 | + "mixer" 58 | + str(mixerstr) 59 | + "_" 60 | "casename" 61 | + str(casename) 62 | + "_" 63 | + "shots" 64 | + str(shots) 65 | ) 66 | print("Now running", string_identifier) 67 | 68 | if k == 3: 69 | kf = 4 70 | elif k in [5,6,7]: 71 | kf = 8 72 | 73 | if method == "fullH": 74 | problem = MaxKCutBinaryFullH( 75 | G, 76 | k, 77 | color_encoding=clf, 78 | ) 79 | 80 | if mixerstr == "X": 81 | mixer = mixers.X() 82 | else: 83 | mixer = MaxKCutGrover( 84 | kf, 85 | problem_encoding=problem_encoding, 86 | color_encoding="all", 87 | tensorized=True, 88 | ) 89 | 90 | initialstate = initialstates.Plus() 91 | 92 | else: 93 | problem = MaxKCutBinaryPowerOfTwo( 94 | G, 95 | kf, 96 | ) 97 | 98 | if mixerstr == "LX": 99 | mixer = MaxKCutLX(k, color_encoding="LessThanK") 100 | elif mixerstr == "Grover": 101 | mixer = MaxKCutGrover( 102 | k, 103 | problem_encoding=problem_encoding, 104 | color_encoding="LessThanK", 105 | tensorized=False, 106 | ) 107 | else: 108 | mixer = MaxKCutGrover( 109 | k, 110 | problem_encoding=problem_encoding, 111 | color_encoding="LessThanK", 112 | tensorized=True, 113 | ) 114 | initialstate = MaxKCutFeasible( 115 | k, problem_encoding=problem_encoding, color_encoding="LessThanK" 116 | ) 117 | 118 | fn = string_identifier + ".pickle" 119 | if os.path.exists(fn): 120 | try: 121 | with open(fn, "rb") as f: 122 | qaoa = pickle.load(f) 123 | except ValueError: 124 | print("file exists, but can not open it", fn) 125 | else: 126 | qaoa = QAOA( 127 | problem=problem, 128 | initialstate=initialstate, 129 | mixer=mixer, 130 | backend=AerSimulator(method="automatic", device="GPU"), 131 | shots=shots, 132 | optimizer=optimizer, 133 | sequential=True, 134 | ) 135 | 136 | qaoa.optimize(maxdepth, angles=angles) 137 | 138 | with open(fn, "wb") as f: 139 | pickle.dump(qaoa, f) 140 | 141 | 142 | if __name__ == "__main__": 143 | 144 | method = str(sys.argv[1]) 145 | k = int(sys.argv[2]) 146 | clf = str(sys.argv[3]) 147 | mixerstr = str(sys.argv[4]) 148 | casename = str(sys.argv[5]) 149 | maxdepth = int(sys.argv[6]) 150 | shots = int(sys.argv[7]) 151 | 152 | main( 153 | method, 154 | k, 155 | clf, 156 | mixerstr, 157 | casename, 158 | maxdepth, 159 | shots, 160 | ) 161 | -------------------------------------------------------------------------------- /unittests/test_maxkcut_one_hot_problem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import numpy as np 4 | import networkx as nx 5 | 6 | sys.path.append("../") 7 | 8 | from qaoa.problems import MaxKCutOneHot 9 | 10 | 11 | class TestMaxKCutOneHotProblem(unittest.TestCase): 12 | def __init__(self, methodname): 13 | super().__init__(methodname) 14 | V = np.arange(0, 2, 1) 15 | E = [(0, 1, 1.0)] 16 | self.barbell = nx.Graph() 17 | self.barbell.add_nodes_from(V) 18 | self.barbell.add_weighted_edges_from(E) 19 | 20 | V = np.arange(0, 3, 1) 21 | E = [(0, 1, 1.0), (1, 2, 1.0)] 22 | self.three_node_graph = nx.Graph() 23 | self.three_node_graph.add_nodes_from(V) 24 | self.three_node_graph.add_weighted_edges_from(E) 25 | 26 | def test_binstringToLabels_k2(self): 27 | """ 28 | Test that MaxKCutOneHot.binstringToLabels() outputs correct labels for k = 2. 29 | """ 30 | prob = MaxKCutOneHot(self.barbell, 2) 31 | labels = {"0101": "00", "0110": "01", "1010": "11", "1001": "10"} 32 | for binstring, expected in labels.items(): 33 | computed = prob.binstringToLabels(binstring) 34 | msg = f"string: {binstring}, expected: {expected}, computed: {computed}" 35 | self.assertEqual(expected, computed, msg) 36 | 37 | def test_binstringToLabels_k3(self): 38 | """ 39 | Test that MaxKCutOneHot.binstringToLabels() outputs correct labels for k = 3. 40 | """ 41 | prob = MaxKCutOneHot(self.barbell, 3) 42 | labels = { 43 | "001001": "00", 44 | "001010": "01", 45 | "001100": "02", 46 | "010001": "10", 47 | "010010": "11", 48 | "010100": "12", 49 | "100001": "20", 50 | "100010": "21", 51 | "100100": "22", 52 | } 53 | for binstring, expected in labels.items(): 54 | computed = prob.binstringToLabels(binstring) 55 | msg = f"string: {binstring}, expected: {expected}, computed: {computed}" 56 | self.assertEqual(expected, computed, msg) 57 | 58 | def test_cost_k2_barbell(self): 59 | """ 60 | Test that the cost funciton in MaxKCutBinaryOneHot is correct for k = 2 with the barbell graph. 61 | """ 62 | prob = MaxKCutOneHot(self.barbell, 2) 63 | strings = {"0101": 0, "1001": 1, "0110": 1, "1010": 0} 64 | for string, expected in strings.items(): 65 | computed = prob.cost(string[::-1]) 66 | msg = f"string: {string}, expected: {expected}, computed: {computed}" 67 | self.assertEqual(expected, computed, msg) 68 | 69 | def test_cost_k2_three_node_graph(self): 70 | """ 71 | Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 2 with three-node graph. 72 | """ 73 | prob = MaxKCutOneHot(self.three_node_graph, 2) 74 | strings = {"010101": 0, "100110": 2, "011010": 1, "101010": 0} 75 | for string, expected in strings.items(): 76 | computed = prob.cost(string[::-1]) 77 | msg = f"string: {string}, expected: {expected}, computed: {computed}" 78 | self.assertEqual(expected, computed, msg) 79 | 80 | def test_cost_k3_three_node_graph(self): 81 | """ 82 | Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 3 with three-node graph. 83 | """ 84 | prob = MaxKCutOneHot(self.three_node_graph, 3) 85 | strings = { 86 | "001010100": 2, 87 | "010100001": 2, 88 | "100100010": 1, 89 | "001001001": 0, 90 | "001100100": 1, 91 | "100100100": 0, 92 | } 93 | for string, expected in strings.items(): 94 | computed = prob.cost(string[::-1]) 95 | msg = f"string: {string}, expected: {expected}, computed: {computed}" 96 | self.assertEqual(expected, computed, msg) 97 | 98 | def test_cost_k6_three_node_graph(self): 99 | """ 100 | Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 3 with three-node graph. 101 | """ 102 | prob = MaxKCutOneHot(self.three_node_graph, 6) 103 | strings = { 104 | "000100100000000010": 2, 105 | "100000100000100000": 0, 106 | "010000100000100000": 1, 107 | "000001100000010000": 2, 108 | } 109 | for string, expected in strings.items(): 110 | computed = prob.cost(string[::-1]) 111 | msg = f"string: {string}, expected: {expected}, computed: {computed}" 112 | self.assertEqual(expected, computed, msg) 113 | 114 | 115 | if __name__ == "__main__": 116 | unittest.main() 117 | -------------------------------------------------------------------------------- /qaoa/problems/exactcover_problem.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from .base_problem import Problem 6 | from qiskit import QuantumCircuit, QuantumRegister 7 | 8 | from qiskit.circuit import Parameter 9 | 10 | 11 | class ExactCover(Problem): 12 | """ 13 | Exact cover problem. 14 | 15 | Subclass of the `Problem` class. Contains the methods to create the exact cover problem, which is the problem of whether 16 | it is possible to cover all elements of a set exactly once by using some subsets. 17 | 18 | Attributes: 19 | columns (np.ndarray): Matrix where each column represents a subset. 20 | weights (np.ndarray or None): Optional weights for each subset. Defaults to None. 21 | penalty_factor (float or int): Penalty factor for constraint violations. Defaults to 1. 22 | 23 | Methods: 24 | cost(): Calculates the cost of a given solution. 25 | create_circuit(): Creates a parameterized circuit corresponding to the cost function. 26 | isFeasible(): Checks if a given bitstring represents a feasible solution to the problem. 27 | _exactCover(): Computes the penalty for a given solution vector x, measuring how far it is from being an exact cover. 28 | """ 29 | def __init__( 30 | self, 31 | columns, 32 | weights=None, 33 | penalty_factor=1, 34 | ) -> None: 35 | """ 36 | Args: 37 | columns (np.ndarray): Matrix where each column represents a subset. 38 | weights (np.ndarray or None): Optional weights for each subset. Defaults to None. 39 | penalty_factor (float or int): Penalty factor for constraint violations. Defaults to 1. 40 | """ 41 | super().__init__() 42 | self.columns = columns 43 | self.weights = weights 44 | self.penalty_factor = penalty_factor 45 | 46 | colSize = columns.shape[0] ### Size per column 47 | numColumns = columns.shape[1] ### number of columns/qubits 48 | 49 | self.N_qubits = numColumns 50 | 51 | def cost(self, string): 52 | """ 53 | Calculates the cost so that states where an element is not covered, or covered more than once, will be penalized, whereas 54 | sets that contain elements that are covered exactly once are favored. 55 | 56 | Args: 57 | string (str): Bitstring representing a candidate solution. 58 | """ 59 | x = np.array(list(map(int, string))) 60 | c_e = self.__exactCover(x) 61 | 62 | if self.weights is None: 63 | return -c_e 64 | else: 65 | return -(self.weights @ x + self.penalty_factor * c_e) 66 | 67 | def create_circuit(self): 68 | """ 69 | Creates a parameterized quantum circuit corresponding to the cost function. 70 | """ 71 | q = QuantumRegister(self.N_qubits) 72 | self.circuit = QuantumCircuit(q) 73 | cost_param = Parameter("x_gamma") 74 | 75 | colSize, numColumns = np.shape(self.columns) 76 | 77 | ### cost Hamiltonian 78 | for col in range(numColumns): 79 | hr = ( 80 | self.penalty_factor 81 | * 0.5 82 | * self.columns[:, col] 83 | @ (np.sum(self.columns, axis=1) - 2) 84 | ) 85 | if not self.weights is None: 86 | hr += 0.5 * self.weights[col] 87 | 88 | if not math.isclose(hr, 0, abs_tol=1e-7): 89 | self.circuit.rz(cost_param * hr, q[col]) 90 | 91 | for col_ in range(col + 1, numColumns): 92 | Jrr_ = ( 93 | self.penalty_factor 94 | * 0.5 95 | * self.columns[:, col] 96 | @ self.columns[:, col_] 97 | ) 98 | 99 | if not math.isclose(Jrr_, 0, abs_tol=1e-7): 100 | self.circuit.cx(q[col], q[col_]) 101 | self.circuit.rz(cost_param * Jrr_, q[col_]) 102 | self.circuit.cx(q[col], q[col_]) 103 | 104 | def isFeasible(self, string): 105 | """ 106 | Checks if a given bitstring represents a feasible solution to the exact cover problem. 107 | 108 | Args: 109 | string (str): Bitstring representing a candidate solution. 110 | """ 111 | x = np.array(list(map(int, string))) 112 | c_e = self.__exactCover(x) 113 | return math.isclose(c_e, 0, abs_tol=1e-7) 114 | 115 | def __exactCover(self, x): 116 | """ 117 | Computes the penalty for a given solution vector x, measuring how far it is from being an exact cover. 118 | 119 | Args: 120 | x (np.ndarray): Binary vector representing a candidate solution. 121 | """ 122 | return np.sum((1 - (self.columns @ x)) ** 2) 123 | -------------------------------------------------------------------------------- /qaoa/problems/maxkcut_one_hot_problem.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister 2 | from qiskit.circuit import Parameter 3 | import networkx as nx 4 | 5 | from .base_problem import Problem 6 | 7 | 8 | class MaxKCutOneHot(Problem): 9 | """ 10 | Max k-CUT problem using one-hot encoding. 11 | 12 | Subclass of the `Problem` class. This class formulates the Max k-Cut problem for a given graph using a one-hot encoding for node colors. 13 | It provides methods to convert bitstrings to color labels, compute the cut value and construct the corresponding quantum circuit. 14 | 15 | Attributes: 16 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 17 | k_cuts (int): The number of partitions (colors) to cut the graph into. 18 | num_V (int): The number of nodes in the graph. 19 | N_qubits (int): The total number of qubits (nodes × colors). 20 | 21 | Methods: 22 | binstringToLabels(string): Converts a binary string in one-hot encoding to a string of color labels for each node. 23 | cost(string): Computes the Max k-Cut cost for a given binary string representing a coloring. 24 | create_circuit(): Creates the parameterized quantum circuit corresponding to the Max k-Cut cost function using one-hot encoding. 25 | """ 26 | def __init__(self, G: nx.Graph, k_cuts: int) -> None: 27 | """ 28 | Args: 29 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 30 | k_cuts (int): The number of partitions (colors) to cut the graph into. 31 | 32 | Raises: 33 | ValueError: If k_cuts is less than 2 or greater than 8. 34 | """ 35 | super().__init__() 36 | if (k_cuts < 2) or (k_cuts > 8): 37 | raise ValueError( 38 | "k_cuts must be 2 or more, and is not implemented for k_cuts > 8" 39 | ) 40 | self.G = G 41 | self.num_V = self.G.number_of_nodes() 42 | self.k_cuts = k_cuts 43 | self.N_qubits = self.num_V * self.k_cuts 44 | 45 | def binstringToLabels(self, string: str) -> str: 46 | """ 47 | Converts a binary string in one-hot encoding to a string of color labels for each node. 48 | 49 | Args: 50 | string (str): The binary string representing the one-hot encoding of node colors. 51 | 52 | Raises: 53 | ValueError: If a segment of the string does not represent a valid one-hot encoding. 54 | 55 | Returns: 56 | labels (str): String of color labels for each node. 57 | """ 58 | k = self.k_cuts 59 | labels = "" 60 | for v in range(self.num_V): 61 | segment = string[v * k : (v + 1) * k] 62 | rev = segment[::-1] 63 | idx = rev.find("1") 64 | if idx == -1: 65 | raise ValueError( 66 | f"Segment {segment} from {string} is not a valid encoding" 67 | ) 68 | labels += str(idx) 69 | return labels 70 | 71 | def cost(self, string: str) -> float | int: 72 | """ 73 | Computes the Max k-Cut cost for a given binary string representing a coloring. 74 | 75 | Args: 76 | string (str): The binary string representing the one-hot encoding of node colors. 77 | 78 | Returns: 79 | C (float or int): The total cut value for the given coloring. 80 | """ 81 | labels = self.binstringToLabels(string) 82 | C = 0 83 | for edge in self.G.edges(): 84 | i = edge[0] 85 | j = edge[1] 86 | li = min(self.k_cuts - 1, int(labels[int(i)])) 87 | lj = min(self.k_cuts - 1, int(labels[int(j)])) 88 | if li != lj: 89 | w = self.G[edge[0]][edge[1]]["weight"] 90 | C += w 91 | return C 92 | 93 | def create_circuit(self) -> None: 94 | """ 95 | Creates the parameterized quantum circuit corresponding to the Max k-Cut cost function using one-hot encoding. 96 | 97 | """ 98 | q = QuantumRegister(self.N_qubits) 99 | c = ClassicalRegister(self.N_qubits) 100 | self.circuit = QuantumCircuit(q, c) 101 | 102 | cost_param = Parameter("x_gamma") 103 | 104 | # the objective Hamiltonian 105 | for edge in self.G.edges(): 106 | i = int(edge[0]) 107 | j = int(edge[1]) 108 | w = self.G[edge[0]][edge[1]]["weight"] 109 | wg = w * cost_param 110 | I = self.k_cuts * i 111 | J = self.k_cuts * j 112 | for k in range(self.k_cuts): 113 | self.circuit.cx(q[I + k], q[J + k]) 114 | self.circuit.rz(wg, q[J + k]) 115 | self.circuit.cx(q[I + k], q[J + k]) 116 | self.circuit.barrier() 117 | -------------------------------------------------------------------------------- /qaoa/problems/portfolio_problem.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from .qubo_problem import QUBO 6 | 7 | 8 | class PortfolioOptimization(QUBO): 9 | """ 10 | Portfolio optimization QUBO. 11 | 12 | Subclass of the `QUBO` class. It reformulates the portfolio optimization problem as a QUBO problem, where the goal is to maximize the expected return while minimizing the risk, subject to a budget constraint. 13 | 14 | Attributes: 15 | risk (float): Risk aversion parameter (weight for the risk term). 16 | budget (int): The total number of assets to select (budget constraint). 17 | cov_matrix (np.ndarray): Covariance matrix of asset returns. 18 | exp_return (np.ndarray): Expected returns for each asset. 19 | penalty (float): Penalty parameter for enforcing the budget constraint. Defaults to 0. 20 | N_qubits (int): Number of assets/qubits in the problem. 21 | 22 | Methods: 23 | cost_nonQUBO(string, penalize): Computes the cost of a given portfolio bitstring, optionally including the penalty term. 24 | isFeasible(string): Checks if a given bitstring satisfies the budget constraint. 25 | __str2np(s): Converts a bitstring to a numpy array of integers. 26 | """ 27 | def __init__(self, risk, budget, cov_matrix, exp_return, penalty=0) -> None: 28 | """ 29 | 30 | Args: 31 | risk (float): Risk aversion parameter (weight for the risk term). 32 | budget (int): The total number of assets to select (budget constraint). 33 | cov_matrix (np.ndarray): Covariance matrix of asset returns. 34 | exp_return (np.ndarray): Expected returns for each asset. 35 | penalty (float): Penalty parameter for enforcing the budget constraint. Defaults to 0. 36 | """ 37 | self.risk = risk 38 | self.budget = budget 39 | self.cov_matrix = cov_matrix 40 | self.exp_return = exp_return 41 | self.penalty = penalty 42 | self.N_qubits = len(self.exp_return) 43 | 44 | # Reformulated as a QUBO 45 | # min x^T Q x + c^T x + b 46 | # Writing Q as lower triangular matrix since it otherwise is symmetric 47 | Q = self.risk * np.tril( 48 | self.cov_matrix + np.tril(self.cov_matrix, k=-1) 49 | ) + self.penalty * ( 50 | np.eye(self.N_qubits) 51 | + 2 * np.tril(np.ones((self.N_qubits, self.N_qubits)), k=-1) 52 | ) 53 | c = -self.exp_return - ( 54 | 2 * self.penalty * self.budget * np.ones_like(self.exp_return) 55 | ) 56 | b = self.penalty * self.budget * self.budget 57 | 58 | super().__init__(Q=Q, c=c, b=b) 59 | 60 | def cost_nonQUBO(self, string, penalize=True): 61 | """ 62 | Computes the cost of a given portfolio bitstring, optionally including the penalty term for the budget constraint. 63 | 64 | Args: 65 | string (str): Bitstring representing the selected assets (portfolio). 66 | penalize (bool): Whether to include the penalty term for violating the budget constraint. 67 | 68 | Returns: 69 | cost (float): The negative of the portfolio objective value. 70 | """ 71 | # risk = self.params.get("risk") 72 | # budget = self.params.get("budget") 73 | # cov_matrix = self.params.get("cov_matrix") 74 | # exp_return = self.params.get("exp_return") 75 | # penalty = self.params.get("penalty", 0.0) 76 | 77 | x = np.array(list(map(int, string))) 78 | cost = risk * (x.T @ cov_matrix @ x) - exp_return.T @ x 79 | if penalize: 80 | cost += penalty * (x.sum() - budget) ** 2 81 | 82 | return -cost 83 | 84 | def isFeasible(self, string): 85 | """ 86 | Checks if a given bitstring satisfies the budget constraint. 87 | 88 | Args: 89 | string (str): Bitstring representing the selected assets (portfolio). 90 | 91 | Returns: 92 | bool: True if the bitstring satisfies the budget constraint, False otherwise. 93 | """ 94 | x = self.__str2np(string) 95 | constraint = np.sum(x) - self.budget 96 | return math.isclose(constraint, 0, abs_tol=1e-7) 97 | 98 | def __str2np(self, s): 99 | """ 100 | Converts a bitstring to a numpy array of integers. 101 | 102 | Args: 103 | s (str): Bitstring representing the selected assets (portfolio). 104 | 105 | Returns: 106 | x (np.ndarray): Numpy array of integers corresponding to the bitstring. 107 | """ 108 | x = np.array(list(map(int, s))) 109 | assert len(x) == len(self.exp_return), ( 110 | "bitstring " 111 | + s 112 | + " of wrong size. Expected " 113 | + str(len(self.exp_return)) 114 | + " but got " 115 | + str(len(x)) 116 | ) 117 | return x 118 | -------------------------------------------------------------------------------- /agent/explainer.py: -------------------------------------------------------------------------------- 1 | from langchain.prompts import PromptTemplate 2 | from langchain.chains import ConversationalRetrievalChain, LLMChain 3 | from langchain.memory import ConversationBufferMemory 4 | from langchain.chat_models import init_chat_model 5 | 6 | # ----- Helper imports ----- 7 | from saveembedding import SaveEmbedding 8 | from pathlib import Path 9 | 10 | 11 | class Explainer: 12 | def __init__(self, memory=None, embedding=None, model="openai:gpt-4.1", temperature=0): 13 | """ 14 | Initialize the Explainer with the context for QAOA package components. 15 | 16 | Args: 17 | description (str): The description of the parts to explain. 18 | model (str): The language model to use. 19 | temperature (float): The temperature for the language model. 20 | """ 21 | self.llm = init_chat_model(model, temperature=temperature) 22 | file_path = self.file_path() 23 | self.embedding = embedding 24 | self.context = "" 25 | 26 | if embedding is not None: 27 | # Making paths where the embeddings are saved and getting the directory of the current file 28 | current_dir = Path(__file__).resolve().parent 29 | persist_path = str(current_dir / "embeddings" / "Explainer_embedding") 30 | cache_path = str(current_dir / "embeddings" / "Explainer_cache") 31 | 32 | 33 | # Create or extract an embedding that is saved in the persist_path 34 | make_or_get_embedding = SaveEmbedding( 35 | dir_paths=file_path, 36 | collection_name="Explainer_embedding", 37 | persist_path=persist_path, 38 | cache_path=cache_path, 39 | ) 40 | 41 | # Get the context, vectorstore, and retriever from the embedding 42 | self.context = make_or_get_embedding.get_context() 43 | self.vectorstore = make_or_get_embedding.get_vectorstore() 44 | self.retriever = make_or_get_embedding.get_retriever() 45 | 46 | if memory is not None: 47 | self.memory = memory 48 | else: 49 | self.memory = ConversationBufferMemory( 50 | memory_key="chat_history", 51 | input_key="question", 52 | return_messages=True, 53 | ) 54 | self.prompt = PromptTemplate( 55 | input_variables=["question", "context", "chat_history"], 56 | template=""" 57 | You are an expert on the QAOA package. You get a list over what the USER wants you to explain (they can be for example classes, methods, etc.) and you are going to explain how they work and what attributes, args, and returns they have. 58 | 59 | The parts you want to explain are: {question} 60 | Your context is the documentation strings for the code: {context} 61 | Here is the chat history: {chat_history} 62 | 63 | Make it helpful so that the USER understand the overall meaning of the parts of the package and also how it is used in a code. 64 | If you are explaining a method, include the class it belongs to. If you are explaining a class, include its methods and attributes there are any. If you are explaining a variable, include its type and purpose. 65 | Be concise and structured. 66 | If the USER asks for a specific part of the QAOA package, make sure to explain that part in detail. 67 | Make subtitles for each part you explain, and do NOT use lists or numbered lists. 68 | NEVER include code snippets in your response. 69 | Do not include anything the USER has not asked for. 70 | """, 71 | ) 72 | # Initialize the chain with the LLM and prompt 73 | if embedding is not None: 74 | self.chain = ConversationalRetrievalChain.from_llm( 75 | llm=self.llm, 76 | memory=self.memory, 77 | retriever=self.retriever, 78 | combine_docs_chain_kwargs={"prompt": self.prompt}, 79 | ) 80 | else: 81 | self.chain = LLMChain(llm=self.llm, prompt=self.prompt, memory=self.memory) 82 | 83 | def file_path(self): 84 | """Set or update the context variable with documentation.""" 85 | script_dir = Path(__file__).resolve().parent.parent 86 | 87 | # Path to the 'qaoa' folder next to it 88 | folder_path = script_dir / "qaoa" 89 | 90 | # Only get .py files for docstring extraction 91 | py_file_paths = [ 92 | str(file) for file in folder_path.rglob("*.py") if file.is_file() 93 | ] 94 | 95 | return py_file_paths 96 | 97 | def explain(self, question): 98 | """Generate an explanation using the specified context chunk.""" 99 | # Invoke the chain with the question and context 100 | result = self.chain.invoke({"question": question, "context": self.context}) 101 | if self.embedding is not None: 102 | return result.get("answer", result) 103 | else: 104 | return result.get("text", result) 105 | 106 | -------------------------------------------------------------------------------- /qaoa/initialstates/maxkcut_feasible_initialstate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | from .base_initialstate import InitialState 5 | from .dicke_initialstate import Dicke 6 | from .dicke1_2_initialstate import Dicke1_2 7 | from .lessthank_initialstate import LessThanK 8 | from .tensor_initialstate import Tensor 9 | 10 | 11 | class MaxKCutFeasible(InitialState): 12 | """ 13 | Initial state for the MAX k-CUT problem that only includes feasible states in the case where one uses the power-of-two problem Hamiltonian. 14 | 15 | Subclass of the `InitialState` class. For k not a power of two, the initial state is a superposition of k feasible states, which is created by the `LessThanK` initial state class. 16 | 17 | Attributes: 18 | k_cuts (int): The number of cuts in the MAX k-CUT problem is separated into. 19 | problem_encoding (str): Problem encoding, either "onehot" (using k qubits per state with one being 1, the position of which corresponing to the color, and the rest 0) or "binary" (binary representation of k). 20 | color_encoding (str): The approach to solving the MAX k-cut problem by following one of three methods: 21 | - "Dicke1_2", used for onehot encoding 22 | - "LessThanK" used for generating a subset of k computational basis states 23 | - "max_balanced" (which corresponds to the onehot case where a color corresponds to a state) 24 | 25 | Methods: 26 | create_circuit(): Generates the circuit that creates the initial state from the |0> state. 27 | """ 28 | def __init__( 29 | self, k_cuts: int, problem_encoding: str, color_encoding: str = "LessThanK" 30 | ) -> None: 31 | """ 32 | Args: 33 | k_cuts (int): The number of cuts in the MAX k-CUT problem is separated into. 34 | problem_encoding (str): Problem encoding, either "onehot" (using k qubits per state with one being 1, the position of which corresponing to the color, and the rest 0) or "binary" (binary representation of k). 35 | color_encoding (str): The approach to solving the MAX k-cut problem by following one of three methods: 36 | - "Dicke1_2", used for onehot encoding 37 | - "LessThanK" used for generating a subset of k computational basis states 38 | - "max_balanced" (which corresponds to the onehot case where a color corresponds to a state) 39 | """ 40 | self.k_cuts = k_cuts 41 | self.problem_encoding = problem_encoding 42 | self.color_encoding = color_encoding 43 | 44 | if not problem_encoding in ["onehot", "binary"]: 45 | raise ValueError('case must be in ["onehot", "binary"]') 46 | if problem_encoding == "binary": 47 | if k_cuts == 6 and (color_encoding not in ["Dicke1_2", "LessThanK"]): 48 | raise ValueError('color_encoding must be in ["LessThanK", "Dicke1_2"]') 49 | self.color_encoding = color_encoding 50 | 51 | if self.k_cuts == 3: 52 | self.infeasible = ["11"] 53 | elif self.k_cuts == 5: 54 | if self.color_encoding == "max_balanced": 55 | self.infeasible = ["100", "111", "101"] 56 | else: 57 | self.infeasible = ["101", "110", "111"] 58 | elif self.k_cuts == 6: 59 | if self.color_encoding in ["Dicke1_2", "max_balanced"]: 60 | self.infeasible = ["000", "111"] 61 | else: 62 | self.infeasible = ["110", "111"] 63 | elif self.k_cuts == 7: 64 | self.infeasible = ["111"] 65 | 66 | def create_circuit(self) -> None: 67 | """ 68 | Generates the circuit that creates the initial state from the |0> state. 69 | """ 70 | if self.problem_encoding == "binary": 71 | self.k_bits = int(np.ceil(np.log2(self.k_cuts))) 72 | self.num_V = self.N_qubits / self.k_bits 73 | 74 | if not self.num_V.is_integer(): 75 | raise ValueError( 76 | "Total qubits=" 77 | + str(self.N_qubits) 78 | + " is not a multiple of " 79 | + str(self.k_bits) 80 | ) 81 | if self.k_cuts == 6 and self.color_encoding == "Dicke1_2": 82 | circ_one_node = Dicke1_2() 83 | else: 84 | circ_one_node = LessThanK(self.k_cuts) 85 | 86 | elif self.problem_encoding == "onehot": 87 | self.num_V = self.N_qubits / self.k_cuts 88 | 89 | if not self.num_V.is_integer(): 90 | raise ValueError( 91 | "Total qubits=" 92 | + str(self.N_qubits) 93 | + " is not a multiple of " 94 | + str(self.k_cuts) 95 | ) 96 | self.num_V = int(self.num_V) 97 | 98 | circ_one_node = Dicke(1) 99 | circ_one_node.setNumQubits(self.k_cuts) 100 | 101 | self.num_V = int(self.num_V) 102 | self.tensor = Tensor(circ_one_node, self.num_V) 103 | 104 | self.tensor.create_circuit() 105 | self.circuit = self.tensor.circuit -------------------------------------------------------------------------------- /qaoa/util/statistic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Statistic: 5 | """ 6 | Class for collecting statistics on samples, including expectation value, variance, 7 | maximum, minimum, and Conditional Value at Risk (CVaR). 8 | 9 | See: https://fanf2.user.srcf.net/hermes/doc/antiforgery/stats.pdf 10 | 11 | Attributes: 12 | cvar (float): Conditional Value at Risk threshold, default is 1. 13 | W (float): Total weight of samples. 14 | maxval (float): Maximum value observed. 15 | minval (float): Minimum value observed. 16 | minSols (list): List of strings corresponding to minimum values. 17 | maxSols (list): List of strings corresponding to maximum values. 18 | E (float): Expectation value of the samples. 19 | S (float): Variance of the samples. 20 | all_values (np.ndarray): Array to store all sample values for CVaR calculation. 21 | 22 | Methods: 23 | reset(): Resets all statistics to initial values. 24 | add_sample(value, weight, string): Adds a sample value with its weight and associated string. 25 | get_E(): Returns the expectation value. 26 | get_Variance(): Returns the variance of the samples. 27 | get_max(): Returns the maximum value observed. 28 | get_min(): Returns the minimum value observed. 29 | get_max_sols(): Returns the list of strings corresponding to maximum values. 30 | get_min_sols(): Returns the list of strings corresponding to minimum values. 31 | get_CVaR(): Returns the Conditional Value at Risk based on the samples. 32 | """ 33 | 34 | def __init__(self, cvar=1): 35 | """ 36 | Initializes the Statistic class with a specified CVaR threshold. 37 | 38 | Args: 39 | cvar (int, optional): CVaR threshold. Defaults to 1. 40 | """ 41 | self.cvar = cvar 42 | self.reset() 43 | 44 | def reset(self): 45 | """ 46 | Resets all statistics to their initial values. 47 | """ 48 | self.W = 0 49 | self.maxval = float("-inf") 50 | self.minval = float("inf") 51 | self.minSols = [] 52 | self.maxSols = [] 53 | self.E = 0 54 | self.S = 0 55 | self.all_values = np.array([]) 56 | 57 | def add_sample(self, value, weight, string): 58 | """ 59 | Adds a sample value with its weight and associated string to the statistics. 60 | 61 | Args: 62 | value (float): The value of the sample. 63 | weight (float): The weight of the sample. 64 | string (str): The string associated with the sample. 65 | """ 66 | self.W += weight 67 | tmp_E = self.E 68 | if value >= self.maxval: 69 | if value == self.maxval: 70 | self.maxSols.append(string) 71 | else: 72 | self.maxval = value 73 | self.maxSols = [string] 74 | if value <= self.minval: 75 | if value == self.minval: 76 | self.minSols.append(string) 77 | else: 78 | self.minval = value 79 | self.minSols = [string] 80 | 81 | self.maxval = max(value, self.maxval) 82 | self.minval = min(value, self.minval) 83 | self.E += weight / self.W * (value - self.E) 84 | self.S += weight * (value - tmp_E) * (value - self.E) 85 | if self.cvar < 1: 86 | idx = np.searchsorted(self.all_values, value) 87 | self.all_values = np.insert( 88 | self.all_values, idx, np.ones(int(weight)) * value 89 | ) 90 | 91 | def get_E(self): 92 | """ 93 | Returns: 94 | float: The expectation value of the samples. 95 | """ 96 | return self.E 97 | 98 | def get_Variance(self): 99 | """ 100 | Returns: 101 | float: The variance of the samples. 102 | """ 103 | return self.S / (self.W - 1) 104 | 105 | def get_max(self): 106 | """ 107 | Returns: 108 | float: The maximum value observed in the samples.""" 109 | return self.maxval 110 | 111 | def get_min(self): 112 | """ 113 | Returns: 114 | float: The minimum value observed in the samples. 115 | """ 116 | return self.minval 117 | 118 | def get_max_sols(self): 119 | """ 120 | Returns: 121 | list: The list of strings corresponding to the maximum values observed. 122 | """ 123 | return self.maxSols 124 | 125 | def get_min_sols(self): 126 | """ 127 | Returns: 128 | list: The list of strings corresponding to the minimum values observed. 129 | """ 130 | return self.minSols 131 | 132 | def get_CVaR(self): 133 | """ 134 | Returns: 135 | float: The CVaR based on the samples. 136 | """ 137 | if self.cvar < 1: 138 | cvarK = int(np.round(self.cvar * len(self.all_values))) 139 | cvar = np.sum(self.all_values[-cvarK:]) / cvarK 140 | return cvar 141 | else: 142 | return self.get_E() 143 | -------------------------------------------------------------------------------- /qaoa/initialstates/dicke_initialstate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from qiskit import QuantumCircuit, QuantumRegister 4 | from qiskit.circuit import Parameter 5 | from qiskit.circuit.library import RYGate 6 | 7 | from .base_initialstate import InitialState 8 | 9 | 10 | class Dicke(InitialState): 11 | """ 12 | Dicke initial state. 13 | 14 | Subclass of the `InitialState` class. Creates a circuit representing a Dicke state with Hamming weight k. 15 | 16 | Attributes: 17 | k (int): The Hamming weight of the Dicke states. 18 | 19 | Methods: 20 | create_circuit(): Creates the circuit to prepare the Dicke states. 21 | """ 22 | def __init__(self, k) -> None: 23 | """ 24 | Args: 25 | k (int): The Hamming weight of the Dicke states. 26 | """ 27 | super().__init__() 28 | self.k = k 29 | 30 | def create_circuit(self): 31 | """ 32 | Circuit to prepare Dicke states, following the algorithm from https://arxiv.org/pdf/1904.07358.pdf. 33 | """ 34 | 35 | q = QuantumRegister(self.N_qubits) 36 | self.circuit = QuantumCircuit(q) 37 | 38 | self.circuit.x(q[-self.k :]) 39 | 40 | for l in range(self.k + 1, self.N_qubits + 1)[::-1]: 41 | self.circuit.append( 42 | Dicke.getBlock1(self.N_qubits, self.k, l), range(self.N_qubits) 43 | ) 44 | 45 | for l in range(2, self.k + 1)[::-1]: 46 | self.circuit.append( 47 | Dicke.getBlock2(self.N_qubits, self.k, l), range(self.N_qubits) 48 | ) 49 | 50 | @staticmethod 51 | def getRYi(n): 52 | """ 53 | Returns gate (i) from section 2.2. 54 | 55 | Args: 56 | n (int): The integer parameter for gate (i). 57 | 58 | Returns: 59 | QuantumCircuit: Quantum circuit representing gate (i). 60 | """ 61 | 62 | qc = QuantumCircuit(2) 63 | 64 | qc.cx(0, 1) 65 | theta = 2 * np.arccos(np.sqrt(1 / n)) 66 | ry = RYGate(theta).control(ctrl_state="1") 67 | qc.append(ry, [1, 0]) 68 | qc.cx(0, 1) 69 | 70 | return qc 71 | 72 | @staticmethod 73 | def getRYii(l, n): 74 | """ 75 | Returns gate (ii)_l from section 2.2. 76 | 77 | Args: 78 | l (int): The integer parameter for gate (ii)_l. 79 | n (int): The integer parameter for gate (ii)_l. 80 | 81 | Returns: 82 | QuantumCircuit: Quantum circuit representing gate (ii)_l. 83 | """ 84 | 85 | qc = QuantumCircuit(3) 86 | 87 | qc.cx(0, 2) 88 | theta = 2 * np.arccos(np.sqrt(l / n)) 89 | ry = RYGate(theta).control(num_ctrl_qubits=2, ctrl_state="11") 90 | qc.append(ry, [2, 1, 0]) 91 | qc.cx(0, 2) 92 | 93 | return qc 94 | 95 | @staticmethod 96 | def getSCS(n, k): 97 | """ 98 | Returns SCS_{n,k} gate from definition 3. 99 | 100 | Args: 101 | n (int): The integer parameter for SCS_{n,k}. 102 | k (int): The integer parameter for SCS_{n,k}. 103 | 104 | Returns: 105 | QuantumCircuit: Quantum circuit representing SCS_{n,k}. 106 | """ 107 | 108 | qc = QuantumCircuit(k + 1) 109 | 110 | qc.append(Dicke.getRYi(n), [k - 1, k]) 111 | for l in range(2, k + 1): 112 | qc.append(Dicke.getRYii(l, n), [k - l, k - l + 1, k]) 113 | 114 | return qc 115 | 116 | @staticmethod 117 | def getBlock1(n, k, l): 118 | """ 119 | Returns the first block in Lemma 2. 120 | 121 | Args: 122 | n (int): The integer parameter for the quantum register size. 123 | k (int): The integer parameter for the Hamming weight. 124 | l (int): The integer parameter for the block. 125 | 126 | Returns: 127 | QuantumCircuit: Quantum circuit representing the first block in Lemma 2. 128 | """ 129 | 130 | qr = QuantumRegister(n) 131 | qc = QuantumCircuit(qr) 132 | 133 | first = l - k - 1 134 | last = n - l 135 | 136 | index = list(range(n)) 137 | 138 | if first != 0: 139 | index = index[first:] 140 | 141 | if last != 0: 142 | index = index[:-last] 143 | qc.append(Dicke.getSCS(l, k), index) 144 | else: 145 | qc.append(Dicke.getSCS(l, k), index) 146 | 147 | return qc 148 | 149 | @staticmethod 150 | def getBlock2(n, k, l): 151 | """ 152 | Returns the second block from Lemma 2. 153 | 154 | Args: 155 | n (int): The integer parameter for the quantum register size. 156 | k (int): The integer parameter for the Hamming weight. 157 | l (int): The integer parameter for the block. 158 | 159 | Returns: 160 | QuantumCircuit: Quantum circuit representing the second block in Lemma 2. 161 | """ 162 | 163 | qr = QuantumRegister(n) 164 | qc = QuantumCircuit(qr) 165 | 166 | last = n - l 167 | index = list(range(n)) 168 | 169 | if last != 0: 170 | index = index[:-last] 171 | qc.append(Dicke.getSCS(l, l - 1), index) 172 | else: 173 | qc.append(Dicke.getSCS(l, l - 1), index) 174 | 175 | return qc 176 | -------------------------------------------------------------------------------- /qaoa/mixers/maxkcut_grover_mixer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from qaoa.mixers import Mixer, Grover 4 | from qaoa.initialstates import Dicke, Plus 5 | 6 | from qaoa.initialstates.dicke1_2_initialstate import Dicke1_2 7 | from qaoa.initialstates.lessthank_initialstate import LessThanK 8 | from qaoa.initialstates.tensor_initialstate import Tensor 9 | 10 | 11 | class MaxKCutGrover(Mixer): 12 | """ 13 | Grover mixer for the Max K-Cut problem. 14 | 15 | Subclass of the `Mixer` subclass that implements the Grover mixing operation for the Max k-cut problem. 16 | 17 | Attributes: 18 | k_cuts (int): The number of cuts in the Max k-Cut problem. 19 | problem_encoding (str): The encoding of the problem, either "onehot" or "binary". 20 | color_encoding (str): The encoding of colors, can be "max_balanced", "Dicke1_2" or "LessThanK". 21 | tensorized (bool): Whether to use tensorization for the mixer. 22 | 23 | Methods: 24 | is_power_of_two(): Returns True if `k_cuts` is a power of two, False otherwise. 25 | set_numV(k): Sets the number of vertices based on the number of cuts. 26 | create_circuit(): Constructs the Grover mixer circuit for the Max k-Cut problem. 27 | """ 28 | 29 | def __init__( 30 | self, k_cuts: int, problem_encoding: str, color_encoding: str, tensorized: bool 31 | ) -> None: 32 | """ 33 | Initializes the MaxKCutGrover mixer. 34 | 35 | Args: 36 | k_cuts (int): The number of cuts in the Max k-Cut problem. 37 | problem_encoding (str): The encoding of the problem, either "onehot" or "binary". 38 | color_encoding (str): The encoding of colors, can be "max_balanced", "Dicke1_2" or "LessThanK". 39 | tensorized (bool): Whether to use tensorization for the mixer. 40 | 41 | Raises: 42 | ValueError: If `k_cuts` is less than 2 or greater than 8, or if `problem_encoding` is not valid. 43 | ValueError: If `color_encoding` is not valid for the given `k_cuts`. 44 | """ 45 | if (k_cuts < 2) or (k_cuts > 8): 46 | raise ValueError( 47 | "k_cuts must be 2 or more, and is not implemented for k_cuts > 8" 48 | ) 49 | if not problem_encoding in ["onehot", "binary"]: 50 | raise ValueError('problem_encoding must be in ["onehot", "binary"]') 51 | self.k_cuts = k_cuts 52 | self.problem_encoding = problem_encoding 53 | self.color_encoding = color_encoding 54 | self.tensorized = tensorized 55 | 56 | if (self.problem_encoding == "binary") and self.is_power_of_two(): 57 | print( 58 | "k_cuts is a power of two. You might want to use the X-mixer instead." 59 | ) 60 | 61 | # for k=6, max_balanced == Dicke1_2 62 | if k_cuts == 6 and ( 63 | color_encoding not in ["max_balanced", "Dicke1_2", "LessThanK"] 64 | ): 65 | raise ValueError( 66 | 'color_encoding must be in ["LessThanK", "Dicke1_2", max_balanced]' 67 | ) 68 | 69 | def is_power_of_two(self) -> bool: 70 | """ 71 | Return True if self.k_cuts is a power of two, False otherwise. 72 | """ 73 | if self.k_cuts > 0 and (self.k_cuts & (self.k_cuts - 1)) == 0: 74 | return True 75 | return False 76 | 77 | def set_numV(self, k): 78 | """ 79 | Set the number of vertices based on the number of cuts. 80 | 81 | Args: 82 | k (int): The number of cuts in the Max k-Cut problem. 83 | 84 | Raises: 85 | ValueError: If the total number of qubits is not a multiple of k. 86 | """ 87 | num_V = self.N_qubits / k 88 | 89 | if not num_V.is_integer(): 90 | raise ValueError( 91 | "Total qubits=" + str(self.N_qubits) + " is not a multiple of " + str(k) 92 | ) 93 | 94 | self.num_V = int(num_V) 95 | 96 | def create_circuit(self) -> None: 97 | """ 98 | Constructs the Grover mixer circuit for the Max k-Cut problem. 99 | """ 100 | 101 | if self.problem_encoding == "binary": 102 | self.k_bits = int(np.ceil(np.log2(self.k_cuts))) 103 | self.set_numV(self.k_bits) 104 | 105 | if self.is_power_of_two(): 106 | circ_one_node = Plus() 107 | circ_one_node.N_qubits = self.k_bits 108 | elif self.k_cuts == 6 and self.color_encoding in [ 109 | "max_balanced", 110 | "Dicke1_2", 111 | ]: 112 | circ_one_node = Dicke1_2() 113 | else: 114 | circ_one_node = LessThanK(self.k_cuts) 115 | 116 | elif self.problem_encoding == "onehot": 117 | self.set_numV(self.k_cuts) 118 | 119 | circ_one_node = Dicke(1) 120 | circ_one_node.setNumQubits(self.k_cuts) 121 | 122 | if self.tensorized: 123 | gm = Grover(circ_one_node) 124 | 125 | tensor_gm = Tensor(gm, self.num_V) 126 | 127 | tensor_gm.create_circuit() 128 | self.circuit = tensor_gm.circuit 129 | else: 130 | tensor_feas = Tensor(circ_one_node, self.num_V) 131 | 132 | gm = Grover(tensor_feas) 133 | 134 | gm.create_circuit() 135 | self.circuit = gm.circuit 136 | -------------------------------------------------------------------------------- /qaoa/initialstates/lessthank_initialstate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumCircuit, QuantumRegister 3 | from qiskit.circuit.library import XXPlusYYGate 4 | 5 | from .base_initialstate import InitialState # type: ignore 6 | 7 | 8 | class LessThanK(InitialState): 9 | """ 10 | LessThanK initial state. 11 | 12 | Subclass for the `InitialState` class. Creates a quantum circuit representing the initial state for the for the MAX k-CUT problem. 13 | For k not a power of two, the initial state is a superposition of k feasible states from the computational basis. 14 | 15 | Attributes: 16 | k (int): subsets (or "colors") of the vertices in the MAX k-CUT problem is seperated into. 17 | 18 | Methods: 19 | create_circuit(): Generates a circuit that creates the initial state from the |0> state. 20 | """ 21 | def __init__(self, k: int) -> None: 22 | """ 23 | Checks that the k-value is valid and initizalizes N qubits and k cuts 24 | 25 | Args: 26 | k (int): the number of subsets of the vertices in the MAX k-CUT problem 27 | 28 | Raises: 29 | ValueError: if k is neither a power of 2 or between 2 and 8 30 | """ 31 | if not LessThanK.is_power_of_two_or_between_2_and_8(k): 32 | raise ValueError("k must be a power of two or between 2 and 8") 33 | self.k = k 34 | self.N_qubits = int(np.ceil(np.log2(self.k))) 35 | 36 | def create_circuit(self) -> None: 37 | """ 38 | Creates a circuit by calling on the methods for different k, following the algorithm from https://arxiv.org/abs/2411.08594. 39 | k is between 2 and 8 or a power of 2. 40 | """ 41 | if self.k == 3: 42 | self.circuit = self.k3() 43 | elif self.k == 5: 44 | self.circuit = self.k5() 45 | elif self.k == 6: 46 | self.circuit = self.k6() 47 | elif self.k == 7: 48 | self.circuit = self.k7() 49 | else: 50 | self.circuit = self.power_of_two() 51 | 52 | def is_power_of_two_or_between_2_and_8(k): 53 | """ 54 | Checks the validity of the argument k, so that k is either between 2 and 8 or a power of 2 55 | 56 | Returns: 57 | True if k is a power of 2 or between 2 and 8, and False otherwise 58 | """ 59 | # Check if k is between 2 and 8 60 | if 2 <= k <= 8: 61 | return True 62 | 63 | # Check if k is a power of two 64 | # A number is a power of two if it has exactly one bit set, i.e., k & (k - 1) == 0 and k > 0 65 | if k > 0 and (k & (k - 1)) == 0: 66 | return True 67 | 68 | return False 69 | 70 | def power_of_two(self) -> QuantumCircuit: 71 | """ 72 | Creates a circuit for the case where k is a power of two. 73 | 74 | Returns: 75 | QuantumCircuit: Quantum circuit. 76 | """ 77 | q = QuantumRegister(self.N_qubits) 78 | circuit = QuantumCircuit(q) 79 | circuit.h(q) 80 | return circuit 81 | 82 | def k3(self) -> QuantumCircuit: 83 | """ 84 | Creates a circuit for the case k = 3. 85 | 86 | Returns: 87 | QuantumCircuit: Quantum circuit. 88 | """ 89 | q = QuantumRegister(self.N_qubits) 90 | circuit = QuantumCircuit(q) 91 | theta = np.arccos(1 / np.sqrt(3)) * 2 92 | phi = np.pi / 2 93 | beta = -np.pi / 2 94 | circuit.ry(theta, 1) 95 | gate = XXPlusYYGate(phi, beta) 96 | circuit.append(gate, [0, 1]) 97 | return circuit 98 | 99 | def k5(self) -> QuantumCircuit: 100 | """ 101 | Creates a circuit for the case k = 5. 102 | 103 | Returns: 104 | QuantumCircuit: Quantum circuit. 105 | """ 106 | q = QuantumRegister(self.N_qubits) 107 | circuit = QuantumCircuit(q) 108 | theta = np.arcsin(1 / np.sqrt(5)) * 2 109 | circuit.ry(theta, 0) 110 | circuit.ch(0, [1, 2], ctrl_state=0) 111 | return circuit 112 | 113 | def k6(self) -> QuantumCircuit: 114 | """ 115 | Creates a circuit for the case k = 6. 116 | 117 | Returns: 118 | QuantumCircuit: Quantum circuit. 119 | """ 120 | q = QuantumRegister(self.N_qubits) 121 | circuit = QuantumCircuit(q) 122 | theta = np.pi / 2 123 | phi = np.arccos(1 / np.sqrt(3)) * 2 124 | gamma = np.pi / 2 125 | beta = -np.pi / 2 126 | circuit.ry(theta, 2) 127 | circuit.ry(phi, 1) 128 | gate = XXPlusYYGate(gamma, beta) 129 | circuit.append(gate, [0, 1]) 130 | return circuit 131 | 132 | def k7(self) -> QuantumCircuit: 133 | """ 134 | Creates a circuit for the case k = 7. 135 | 136 | Returns: 137 | QuantumCircuit: uantum circuit. 138 | """ 139 | q = QuantumRegister(self.N_qubits) 140 | circuit = QuantumCircuit(q) 141 | delta = np.arcsin(1 / np.sqrt(7)) * 2 142 | theta = np.pi / 2 143 | phi = np.arccos(1 / np.sqrt(3)) * 2 144 | gamma = np.pi / 2 145 | beta = -np.pi / 2 146 | circuit.ry(delta, 0) 147 | circuit.cx(0, 1) 148 | circuit.cry(theta, 0, 2, ctrl_state=0) 149 | circuit.cry(phi, 0, 1, ctrl_state=0) 150 | gate = XXPlusYYGate(gamma, beta) 151 | circuit.append(gate, [0, 1]) 152 | return circuit 153 | -------------------------------------------------------------------------------- /examples/plotroutines.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from mpl_toolkits.axes_grid1 import make_axes_locatable 3 | from matplotlib.ticker import MaxNLocator 4 | import networkx as nx 5 | 6 | import numpy as np 7 | 8 | from qaoa import QAOA 9 | 10 | from qaoa.util import Statistic 11 | 12 | 13 | def __plot_landscape(A, extent, fig): 14 | if not fig: 15 | fig = plt.figure(figsize=(6, 6), dpi=80, facecolor="w", edgecolor="k") 16 | _ = plt.xlabel(r"$\gamma$") 17 | _ = plt.ylabel(r"$\beta$") 18 | ax = fig.gca() 19 | _ = plt.title("Expectation value") 20 | im = ax.imshow(A, interpolation="bicubic", origin="lower", extent=extent) 21 | divider = make_axes_locatable(ax) 22 | cax = divider.append_axes("right", size="5%", pad=0.05) 23 | _ = plt.colorbar(im, cax=cax) 24 | 25 | 26 | def plot_E(qaoa_instance, fig=None): 27 | angles = qaoa_instance.landscape_p1_angles 28 | extent = [ 29 | angles["gamma"][0], 30 | angles["gamma"][1], 31 | angles["beta"][0], 32 | angles["beta"][1], 33 | ] 34 | return __plot_landscape(qaoa_instance.exp_landscape(), extent, fig=fig) 35 | 36 | 37 | def plot_Var(qaoa_instance, fig=None): 38 | angles = qaoa_instance.landscape_p1_angles 39 | extent = [ 40 | angles["gamma"][0], 41 | angles["gamma"][1], 42 | angles["beta"][0], 43 | angles["beta"][1], 44 | ] 45 | return __plot_landscape(qaoa_instance.var_landscape(), extent, fig=fig) 46 | 47 | 48 | def plot_ApproximationRatio( 49 | qaoa_instance, maxdepth, mincost, maxcost, label, style="", fig=None, shots=None 50 | ): 51 | if not shots: 52 | exp = np.array(qaoa_instance.get_Exp()) 53 | else: 54 | exp = [] 55 | for p in range(1, qaoa_instance.current_depth + 1): 56 | ar, sp = __apprrat_successprob(qaoa_instance, p, shots=shots) 57 | exp.append(ar) 58 | exp = np.array(exp) 59 | 60 | if not fig: 61 | ax = plt.figure().gca() 62 | else: 63 | ax = fig.gca() 64 | plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black") 65 | plt.plot( 66 | np.arange(1, maxdepth + 1), 67 | (maxcost - exp) / (maxcost - mincost), 68 | style, 69 | label=label, 70 | ) 71 | plt.ylim(0, 1.01) 72 | plt.xlim(1 - 0.25, maxdepth + 0.25) 73 | _ = plt.ylabel("appr. ratio") 74 | _ = plt.xlabel("depth") 75 | _ = plt.legend(loc="lower right", framealpha=1) 76 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 77 | 78 | 79 | def plot_successprob(qaoa_instance, maxdepth, label, style="", fig=None, shots=10**4): 80 | successp = [] 81 | for p in range(1, qaoa_instance.current_depth + 1): 82 | ar, sp = __apprrat_successprob(qaoa_instance, p, shots=shots) 83 | successp.append(sp) 84 | successp = np.array(successp) 85 | 86 | if not fig: 87 | ax = plt.figure().gca() 88 | else: 89 | ax = fig.gca() 90 | plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black") 91 | plt.plot( 92 | np.arange(1, maxdepth + 1), 93 | successp, 94 | style, 95 | label=label, 96 | ) 97 | plt.ylim(0, 1.01) 98 | plt.xlim(1 - 0.25, maxdepth + 0.25) 99 | _ = plt.ylabel("success prob") 100 | _ = plt.xlabel("depth") 101 | _ = plt.legend(loc="lower right", framealpha=1) 102 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 103 | 104 | 105 | def __apprrat_successprob(qaoa_instance, depth, shots=10**4): 106 | """ 107 | approximation ratio post processed with feasibility and success probability 108 | """ 109 | hist = qaoa_instance.hist( 110 | qaoa_instance.optimization_results[depth].get_best_angles(), shots=shots 111 | ) 112 | 113 | counts = 0 114 | 115 | stat = Statistic(cvar=qaoa_instance.cvar) 116 | 117 | for string in hist: 118 | if qaoa_instance.problem.isFeasible(string): 119 | cost = qaoa_instance.problem.cost(string) 120 | counts += hist[string] 121 | stat.add_sample(cost, hist[string], string) 122 | 123 | return -stat.get_CVaR(), counts / shots 124 | 125 | 126 | def plot_angles(qaoa_instance, depth, label, style="", fig=None): 127 | angles = qaoa_instance.optimization_results[depth].get_best_angles() 128 | 129 | if not fig: 130 | ax = plt.figure().gca() 131 | else: 132 | ax = fig.gca() 133 | 134 | plt.plot( 135 | np.arange(1, depth + 1), 136 | angles[::2], 137 | "--" + style, 138 | label=r"$\gamma$ " + label, 139 | ) 140 | plt.plot( 141 | np.arange(1, depth + 1), 142 | angles[1::2], 143 | "-" + style, 144 | label=r"$\beta$ " + label, 145 | ) 146 | plt.xlim(1 - 0.25, depth + 0.25) 147 | _ = plt.ylabel("parameter") 148 | _ = plt.xlabel("depth") 149 | _ = plt.legend() 150 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 151 | 152 | 153 | def draw_colored_graph(G, edge_colors): 154 | # Draw the graph with colored edges 155 | # extend the color_map if necessary 156 | color_map = [ 157 | "red", 158 | "blue", 159 | "green", 160 | "purple", 161 | "orange", 162 | "pink", 163 | "brown", 164 | "gray", 165 | "yellow", 166 | "cyan", 167 | ] 168 | pos = nx.circular_layout(G) # Positions for all nodes 169 | 170 | # Draw nodes 171 | nx.draw_networkx_nodes(G, pos, node_size=700, node_color="lightgray") 172 | 173 | # Draw edges with colors 174 | for color_idx, edges in edge_colors.items(): 175 | nx.draw_networkx_edges( 176 | G, 177 | pos, 178 | edgelist=edges, 179 | width=2, 180 | edge_color=color_map[color_idx % len(color_map)], 181 | ) 182 | 183 | # Draw labels 184 | nx.draw_networkx_labels(G, pos, font_size=20, font_family="sans-serif") 185 | 186 | # Show the graph 187 | plt.axis("off") 188 | plt.show() 189 | -------------------------------------------------------------------------------- /qaoa/problems/qubo_problem.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | from qiskit import QuantumCircuit, QuantumRegister 5 | from qiskit.circuit import Parameter 6 | 7 | from .base_problem import Problem 8 | 9 | 10 | class QUBO(Problem): 11 | """ 12 | Quadratic Unconstrained Binary Optimization (QUBO) problem. 13 | 14 | Subclass of the `Problem` class. This class represents a generic QUBO problem, which can be used as a base for more specific QUBO-based problems. 15 | The QUBO problem is defined as minimizing a quadratic function over binary variables. 16 | 17 | Attributes: 18 | Q (np.ndarray): A 2-dimensional numpy ndarray representing the quadratic coefficients. 19 | c (np.ndarray): A 1-dimensional numpy ndarray representing the linear coefficients. 20 | b (float): Scalar offset term. 21 | N_qubits (int): Number of binary variables/qubits in the problem. 22 | lower_triangular_Q (bool): Whether Q is lower triangular. 23 | QUBO_Q (np.ndarray): The quadratic coefficient matrix. 24 | QUBO_c (np.ndarray): The linear coefficient vector. 25 | QUBO_b (float): The scalar offset. 26 | 27 | Methods: 28 | cost(string): Computes the cost of a given binary string according to the QUBO formulation. 29 | create_circuit(): Creates a parametrized quantum circuit corresponding to the cost function of the QUBO problem. 30 | createParameterizedCostCircuitTril(): Creates a parameterized circuit of the triangularized QUBO problem. 31 | """ 32 | def __init__(self, Q=None, c=None, b=None) -> None: 33 | super().__init__() 34 | """ 35 | Implements the mapping from the parameters in params to the QUBO problem. 36 | Is expected to be called by the child class. 37 | 38 | # The QUBO will be on this form: 39 | # min x^T Q x + c^T x + b 40 | 41 | Args: 42 | Q (np.ndarray): A 2-dimensional numpy ndarray representing the quadratic coefficients. 43 | c (np.ndarray): A 1-dimensional numpy ndarray representing the linear coefficients. Defaults to None. 44 | b (float): Scalar offset term. Defaults to None. 45 | 46 | Raises: 47 | AssertionError: If Q is not a square 2D numpy ndarray. 48 | AssertionError: If c is not a 1D numpy ndarray of compatible size. 49 | AssertionError: If b is not a scalar. 50 | """ 51 | assert type(Q) is np.ndarray, "Q needs to be a numpy ndarray, but is " + str( 52 | type(Q) 53 | ) 54 | assert ( 55 | Q.ndim == 2 56 | ), "Q needs to be a 2-dimensional numpy ndarray, but has dim " + str(Q.ndim) 57 | assert Q.shape[0] == Q.shape[1], "Q needs to be a square matrix, but is " + str( 58 | Q.shape 59 | ) 60 | n = Q.shape[0] 61 | 62 | self.N_qubits = n 63 | 64 | # Check if Q is lower triangular 65 | self.lower_triangular_Q = np.allclose(Q, np.tril(Q)) 66 | 67 | self.QUBO_Q = Q 68 | 69 | if c is None: 70 | c = np.zeros(n) 71 | assert type(c) is np.ndarray, "c needs to be a numpy ndarray, but is " + str( 72 | type(c) 73 | ) 74 | assert ( 75 | c.ndim == 1 76 | ), "c needs to be a 1-dimensional numpy ndarray, but has dim " + str(Q.ndim) 77 | assert c.shape[0] == n, ( 78 | "c is of size " 79 | + str(c.shape[0]) 80 | + " but should be compatible size to Q, meaning " 81 | + str(n) 82 | ) 83 | self.QUBO_c = c 84 | 85 | if b is None: 86 | b = 0.0 87 | assert np.isscalar(b), "b is expected to be scalar, but is " + str(b) 88 | self.QUBO_b = b 89 | 90 | def cost(self, string): 91 | """ 92 | Computes the cost of a given binary string according to the QUBO formulation. 93 | 94 | Args: 95 | string (str): Binary string representing a candidate solution to the QUBO problem. 96 | 97 | Returns: 98 | float: The cost of the solution. 99 | """ 100 | x = np.array(list(map(int, string))) 101 | return -(x.T @ self.QUBO_Q @ x + self.QUBO_c.T @ x + self.QUBO_b) 102 | 103 | def create_circuit(self): 104 | """ 105 | Creates a parametrized quantum circuit corresponding to the cost function of the QUBO problem. 106 | 107 | Raises: 108 | NotImplementedError: If Q is not lower triangular. 109 | """ 110 | if not self.lower_triangular_Q: 111 | LOG.error("Function not implemented!", func=self.create_circuit.__name__) 112 | raise NotImplementedError 113 | self.createParameterizedCostCircuitTril() 114 | 115 | def createParameterizedCostCircuitTril(self): 116 | """ 117 | Creates a parameterized circuit of the triangularized QUBO problem. 118 | """ 119 | q = QuantumRegister(self.N_qubits) 120 | self.circuit = QuantumCircuit(q) 121 | cost_param = Parameter("x_gamma") 122 | 123 | ### cost Hamiltonian 124 | for i in range(self.N_qubits): 125 | w_i = 0.5 * (self.QUBO_c[i] + np.sum(self.QUBO_Q[:, i])) 126 | 127 | if not math.isclose(w_i, 0, abs_tol=1e-7): 128 | self.circuit.rz(cost_param * w_i, q[i]) 129 | 130 | for j in range(i + 1, self.N_qubits): 131 | w_ij = 0.25 * self.QUBO_Q[j][i] 132 | 133 | if not math.isclose(w_ij, 0, abs_tol=1e-7): 134 | self.circuit.cx(q[i], q[j]) 135 | self.circuit.rz(cost_param * w_ij, q[j]) 136 | self.circuit.cx(q[i], q[j]) 137 | 138 | # def __str2np(self, s): 139 | # x = np.array(list(map(int, s))) 140 | # assert len(x) == self.N_qubits, ( 141 | # "bitstring " 142 | # + s 143 | # + " of wrong size. Expected " 144 | # + str(self.N_qubits) 145 | # + " but got " 146 | # + str(len(x)) 147 | # ) 148 | # return x 149 | -------------------------------------------------------------------------------- /qaoa/mixers/maxkcut_lx_mixer.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | from qiskit import QuantumCircuit, QuantumRegister 5 | from qiskit.circuit import Parameter 6 | from qiskit.quantum_info import SparsePauliOp, Pauli 7 | from qiskit.circuit.library import PauliEvolutionGate 8 | 9 | from .base_mixer import Mixer 10 | 11 | 12 | class MaxKCutLX(Mixer): 13 | """ 14 | Logical X (LX) mixer for the Max k-Cut problem. 15 | 16 | Subclass of the `Mixer` subclass that implements the LX mixing operation for the Max k-Cut problem. 17 | 18 | Attributes: 19 | k_cuts (int): The number of cuts in the Max k-Cut problem. 20 | color_encoding (str): The encoding of colors, can be "LessThanK", "Dicke1_2" or "max_balanced". 21 | topology (str): The topology of the mixer, either "standard" or "ring". 22 | 23 | Methods: 24 | is_power_of_two(): Returns True if `k_cuts` is a power of two, False otherwise. 25 | create_SparsePauliOp(): Creates the sparse Pauli operator for the given `k_cuts`. 26 | create_circuit(): Constructs the LX mixer circuit for the Max k-Cut problem. 27 | """ 28 | 29 | def __init__(self, k_cuts: int, color_encoding: str, topology: str = "standard"): 30 | """ 31 | Initializes the MaxKCutLX mixer. 32 | 33 | Args: 34 | k_cuts (int): The number of cuts in the Max k-Cut problem. 35 | color_encoding (str): The encoding of colors, can be "LessThanK", "Dicke1_2" or "max_balanced". 36 | topology (str): The topology of the mixer, either "standard" or "ring". 37 | 38 | Raises: 39 | ValueError: If `k_cuts` is a power of two. 40 | ValueError: If `color_encoding` is not specified. 41 | ValueError: If `k_cuts` is 3 and `topology` is not "standard" or "ring". 42 | """ 43 | if (k_cuts < 2) or (k_cuts > 8): 44 | raise ValueError( 45 | "k_cuts must be 2 or more, and is not implemented for k_cuts > 8" 46 | ) 47 | self.k_cuts = k_cuts 48 | self.k_bits = int(np.ceil(np.log2(k_cuts))) 49 | self.color_encoding = color_encoding 50 | self.topology = topology 51 | 52 | if self.is_power_of_two(): 53 | raise ValueError("k_cuts is a power of two. Use e.g. X-mixer instead.") 54 | 55 | if not color_encoding: 56 | raise ValueError("please specify a color encoding") 57 | 58 | if k_cuts == 3 and (topology not in ["standard", "ring"]): 59 | raise ValueError('topology must be in ["standard", "ring"]') 60 | 61 | self.create_SparsePauliOp() 62 | 63 | def is_power_of_two(self) -> bool: 64 | """ 65 | Returns: 66 | bool: True if self.k_cuts is a power of two, False otherwise. 67 | """ 68 | if self.k_cuts > 0 and (self.k_cuts & (self.k_cuts - 1)) == 0: 69 | return True 70 | return False 71 | 72 | def create_SparsePauliOp(self) -> None: 73 | """ 74 | Create sparse Pauli operator for given k. Hard coded. 75 | 76 | Returns: 77 | None 78 | """ 79 | if self.k_cuts == 3: 80 | if self.color_encoding in ["LessThanK"]: 81 | LXM = { 82 | Pauli("IX"): [Pauli("ZI")], 83 | Pauli("XI"): [Pauli("IZ")], 84 | } 85 | if self.topology == "ring": 86 | LXM[Pauli("XX")] = [Pauli("-ZZ")] 87 | else: 88 | raise ValueError("invalid or missing color_encoding") 89 | 90 | elif self.k_cuts == 5: 91 | if self.color_encoding in ["LessThanK"]: 92 | LXM = { 93 | Pauli("IXX"): [Pauli("ZII")], 94 | Pauli("IXI"): [Pauli("ZII")], 95 | Pauli("XII"): [Pauli("IIZ"), Pauli("IZZ"), Pauli("IZI")], 96 | } 97 | else: 98 | raise ValueError("invalid or missing color_encoding") 99 | 100 | elif self.k_cuts == 6: 101 | if self.color_encoding == "LessThanK": 102 | LXM = { 103 | Pauli("IIX"): [], 104 | Pauli("IXI"): [Pauli("ZII")], 105 | Pauli("XII"): [Pauli("IZI")], 106 | } 107 | elif self.color_encoding in ["Dicke1_2", "max_balanced"]: 108 | LXM = { 109 | Pauli("IXX"): [-Pauli("IZZ")], 110 | Pauli("XXI"): [-Pauli("ZZI")], 111 | Pauli("IXI"): [-Pauli("ZIZ")], 112 | } 113 | else: 114 | raise ValueError("invalid or missing color_encoding") 115 | 116 | elif self.k_cuts == 7: 117 | if self.color_encoding == "LessThanK": 118 | LXM = { 119 | Pauli("IIX"): [Pauli("ZII")], 120 | Pauli("IXI"): [Pauli("IIZ")], 121 | Pauli("XII"): [Pauli("IZI")], 122 | } 123 | else: 124 | raise ValueError("invalid or missing color_encoding") 125 | 126 | data = [] 127 | coeffs = [] 128 | 129 | # iterate through LXM dict, 130 | for PX, PZs in LXM.items(): 131 | count = 1 132 | data.append(PX) 133 | for pz in PZs: 134 | composed = PX.compose(pz) 135 | data.append(composed) 136 | count += 1 137 | coeffs += [1 / (len(PZs) + 1)] * (len(PZs) + 1) 138 | self.op = SparsePauliOp(data, coeffs=coeffs) 139 | 140 | def create_circuit(self) -> None: 141 | """ 142 | Constructs the LX mixer circuit for the Max k-Cut problem. 143 | """ 144 | self.num_V = int(self.N_qubits / self.k_bits) 145 | q = QuantumRegister(self.N_qubits) 146 | mixer_param = Parameter("x_beta") 147 | self.circuit = QuantumCircuit(q, name="Mixer") 148 | if math.log(self.k_cuts, 2).is_integer(): 149 | self.circuit.rx(-2 * mixer_param, range(self.N_qubits)) 150 | else: 151 | for v in range(self.num_V): 152 | self.circuit.append( 153 | PauliEvolutionGate(self.op, time=mixer_param), 154 | q[self.k_bits * v : self.k_bits * (v + 1)][::-1], 155 | ) 156 | -------------------------------------------------------------------------------- /agent/saveembedding.py: -------------------------------------------------------------------------------- 1 | # to make the code splitter 2 | from pathlib import Path 3 | from typing import List 4 | import json 5 | 6 | from langchain_core.documents import Document 7 | from langchain_community.document_loaders import DirectoryLoader 8 | from langchain_community.embeddings import OpenAIEmbeddings 9 | from langchain_community.vectorstores import FAISS 10 | from langchain.text_splitter import RecursiveCharacterTextSplitter 11 | 12 | import ast 13 | from langchain.schema import Document 14 | 15 | import os 16 | from langchain_chroma import Chroma 17 | 18 | class SaveEmbedding: 19 | def __init__( 20 | self, 21 | dir_paths, 22 | collection_name, 23 | persist_path, 24 | cache_path, 25 | type_of_receiver="mmr" 26 | ): 27 | self.dir_paths = dir_paths 28 | self.collection_name = collection_name 29 | self.persist_path = persist_path 30 | self.cache_path = cache_path 31 | self.context = None 32 | self.split_docs = None 33 | self.vectorstore = None 34 | self.retriever = None 35 | 36 | self.load_context() 37 | self.split() 38 | self.create_retriever(type_of_receiver) 39 | 40 | def load_python_files(self, repo_path): 41 | """ 42 | Load all Python files from the specified repository path. 43 | """ 44 | loader_py = DirectoryLoader(repo_path, glob="**/*.py") 45 | docs_py = loader_py.load() 46 | return docs_py 47 | 48 | def load_notebook(self, path: Path) -> str: 49 | """Load Jupyter notebook content.""" 50 | with open(path, "r", encoding="utf-8") as f: 51 | notebook = json.load(f) 52 | content = [] 53 | for cell in notebook["cells"]: 54 | if cell["cell_type"] in ["markdown", "code"]: 55 | cell_content = "\n".join(cell["source"]) 56 | content.append(cell_content) 57 | # print(f"Loaded cell content:\n{cell_content}\n{'-'*50}") 58 | return "\n\n".join(content) 59 | 60 | def load_python_script(self, path: Path) -> str: 61 | """Load Python script content.""" 62 | with open(path, "r", encoding="utf-8") as f: 63 | return f.read() 64 | 65 | def load_text_file(self, path: Path) -> str: 66 | """Load text or markdown file content.""" 67 | with open(path, "r", encoding="utf-8") as f: 68 | return f.read() 69 | 70 | def extract_class_docstrings_from_string(self, code: str) -> str: 71 | """Extract class-level docstrings from a Python source string.""" 72 | extracted_docs = [] 73 | try: 74 | tree = ast.parse(code) 75 | for node in ast.walk(tree): 76 | if isinstance(node, ast.ClassDef): 77 | docstring = ast.get_docstring(node) 78 | if docstring: 79 | extracted_docs.append(docstring.strip()) 80 | except SyntaxError: 81 | pass # Skip files that fail to parse 82 | return "\n\n".join(extracted_docs) 83 | 84 | def load_context(self): 85 | """Loads and processes documents from the specified paths.""" 86 | context = [] 87 | for path in self.dir_paths: 88 | path = Path(path) 89 | if not path.exists(): 90 | print(f"File not found - {path}. Skipping.") 91 | continue 92 | if path.suffix == ".ipynb": 93 | context.append(self.load_notebook(path)) 94 | elif path.suffix in [".txt", ".md"]: 95 | context.append(self.load_text_file(path)) 96 | elif path.suffix == ".py": 97 | py_str = self.load_python_script(path) 98 | docstring_str = self.extract_class_docstrings_from_string(py_str) 99 | context.append(docstring_str) 100 | else: 101 | print(f"Unsupported file type - {path.suffix}. Skipping.") 102 | 103 | print(f"\nLoaded {len(context)} files.") 104 | self.context = context 105 | 106 | def split(self): 107 | print(f"Processing {len(self.context)} raw documents.") 108 | try: 109 | docs = [ 110 | Document(page_content=context_str.strip()) 111 | for context_str in self.context 112 | ] 113 | 114 | splitter = RecursiveCharacterTextSplitter( 115 | chunk_size=1500, chunk_overlap=200 116 | ) 117 | split_docs = splitter.split_documents(docs) 118 | print(f"Generated {len(split_docs)} split documents.") 119 | 120 | if not split_docs: 121 | print( 122 | "No documents to process after splitting. Skipping vectorstore creation." 123 | ) 124 | 125 | self.split_docs = split_docs 126 | except Exception as e: 127 | print(f"Error processing documents: {str(e)}") 128 | 129 | def create_retriever(self, type_of_receiver): 130 | if os.path.exists(self.persist_path): 131 | print(f"Loading existing vectorstore from {self.persist_path}") 132 | self.vectorstore = Chroma( 133 | embedding_function=OpenAIEmbeddings(), 134 | persist_directory=self.persist_path, 135 | collection_name=self.collection_name, 136 | ) 137 | else: 138 | # print(self.split_docs) 139 | print(f"Creating new vectorstore at {self.persist_path}") 140 | self.vectorstore = Chroma.from_documents( 141 | documents=self.split_docs, 142 | embedding=OpenAIEmbeddings(), 143 | persist_directory=self.persist_path, 144 | collection_name=self.collection_name, 145 | ) 146 | 147 | if type_of_receiver == "mmr": 148 | self.retriever = self.vectorstore.as_retriever( 149 | search_type="mmr", 150 | search_kwargs={"k": 10, "fetch_k": 20, "lambda_mult": 0.9}, 151 | ) 152 | 153 | else: 154 | self.retriever = self.vectorstore.as_retriever( 155 | search_kwargs={"k": 8} 156 | ) # TODO check out if this is what we want 157 | 158 | def get_retriever(self): 159 | """Get the retriever.""" 160 | return self.retriever 161 | 162 | def get_vectorstore(self): 163 | """Get the vectorstore.""" 164 | return self.vectorstore 165 | 166 | def get_context(self): 167 | """Get the context.""" 168 | return self.context -------------------------------------------------------------------------------- /qaoa/problems/graph_problem.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister, AncillaRegister 2 | from qiskit.circuit import Parameter 3 | from abc import abstractmethod 4 | 5 | from .base_problem import Problem 6 | from qaoa.util import * 7 | 8 | 9 | class GraphProblem(Problem): 10 | """ 11 | Graph problem. 12 | 13 | Subclass of the `Problem` class. Creates a quantum circuit for a general graph problem. 14 | 15 | Attributes: 16 | G: The graph to be used in the problem. 17 | N_qubits_per_node (int): Number of qubits per node. 18 | fix_one_node (bool): If True, fixes the last node to "color1". 19 | 20 | Methods: 21 | create_edge_circuit(theta): Abstract method to create circuit for an edge 22 | create_edge_circuit_fixed_node(theta): Abstract method to create circuit for an edge where one node is fixed 23 | create_circuit(): Creates a circuit for the graph problem. 24 | same_color(str1, str2): Checks if two strings map to the same color. 25 | slice_string(string): Convert a binary string to a list of labels for each node. 26 | cost(string): Creates a cost function for the given solution. 27 | 28 | """ 29 | def __init__( 30 | self, 31 | G, 32 | N_qubits_per_node=1, 33 | fix_one_node: bool = False, # this fixes the last node to color 1, i.e., one qubit gets removed 34 | ) -> None: 35 | """ 36 | Args: 37 | G: The graph to be used in the problem. 38 | N_qubits_per_node (int): Number of qubits per node. 39 | fix_one_node (bool): If True, fixes the last node to "color1". 40 | """ 41 | super().__init__() 42 | 43 | # fixes the last node to "color1" 44 | self.fix_one_node = fix_one_node 45 | 46 | # ensure graph has labels 0, 1, ..., num_V-1 47 | self.graph_handler = GraphHandler(G) 48 | self.num_V = self.graph_handler.G.number_of_nodes() 49 | 50 | self.N_qubits_per_node = N_qubits_per_node 51 | self.N_qubits = (self.num_V - self.fix_one_node) * self.N_qubits_per_node 52 | 53 | self.beta_param = Parameter("gamma") 54 | 55 | @abstractmethod 56 | def create_edge_circuit(self, theta): 57 | """ 58 | Abstract method to create circuit for an edge. 59 | 60 | Args: 61 | theta: Parameter for the edge circuit. 62 | """ 63 | pass 64 | 65 | @abstractmethod 66 | def create_edge_circuit_fixed_node(self, theta): 67 | """ 68 | Abstract method to create circuit for an edge where one node is fixed. 69 | 70 | Args: 71 | theta: Parameter for the edge circuit. 72 | """ 73 | pass 74 | 75 | def create_circuit(self): 76 | """ 77 | Creates a quantum circuit for the graph problem. 78 | """ 79 | q = QuantumRegister(self.N_qubits) 80 | a = AncillaRegister(self.N_ancilla_qubits) 81 | self.circuit = QuantumCircuit(q, a) 82 | 83 | for _, edges in self.graph_handler.parallel_edges.items(): 84 | for edge in edges: 85 | i, j = edge 86 | I = i * self.N_qubits_per_node 87 | J = j * self.N_qubits_per_node 88 | 89 | theta_ij = self.beta_param * self.graph_handler.G[i][j].get("weight", 1) 90 | 91 | if self.num_V - self.fix_one_node not in [i, j]: 92 | qubits_to_map = list(range(I, I + self.N_qubits_per_node)) + list( 93 | range(J, J + self.N_qubits_per_node) 94 | ) 95 | ancilla_to_map = ( 96 | list(range(0, self.N_ancilla_qubits)) 97 | if self.N_ancilla_qubits > 0 98 | else [] 99 | ) 100 | IJcirc = self.create_edge_circuit(theta_ij) 101 | self.circuit.append( 102 | IJcirc, 103 | q[qubits_to_map] 104 | + (a[ancilla_to_map] if ancilla_to_map else []), 105 | ) 106 | else: 107 | # if self.fix_one_node is False, this branch does not exist 108 | minIJ = min(I, J) 109 | minIJcirc = self.create_edge_circuit_fixed_node(theta_ij) 110 | self.circuit.append( 111 | minIJcirc, q[list(range(minIJ, minIJ + self.N_qubits_per_node))] 112 | ) 113 | 114 | # the code below might go into BaseMaxKCut(GraphProblem) 115 | 116 | def same_color(self, str1: str, str2: str) -> bool: 117 | """ 118 | Check if two strings map to the same color. 119 | 120 | Args: 121 | str1 (str): First binary string. 122 | str2 (str): Second binary string. 123 | 124 | Returns: 125 | bool: True if both strings map to the same color, False otherwise. 126 | """ 127 | return self.bitstring_to_color.get(str1) == self.bitstring_to_color.get(str2) 128 | 129 | def slice_string(self, string: str) -> list: 130 | """ 131 | Convert a binary string to a list of labels for each node. 132 | 133 | Args: 134 | string (str): Binary string. 135 | 136 | Returns: 137 | list: List of labels for each node. 138 | """ 139 | k = self.N_qubits_per_node 140 | labels = [ 141 | string[v * k : (v + 1) * k] for v in range(self.num_V - self.fix_one_node) 142 | ] 143 | # Add fixed node label if applicable 144 | if self.fix_one_node: 145 | labels.append(self.colors["color1"][0]) 146 | return labels 147 | 148 | def cost(self, string: str) -> float | int: 149 | """ 150 | Compute the cost for a given solution. 151 | 152 | Args: 153 | string (str): Binary string. 154 | 155 | Raises: 156 | ValueError: If the length of the string does not match the number of qubits. 157 | 158 | Returns: 159 | float | int: The cost of the given solution. 160 | """ 161 | if len(string) != self.N_qubits: 162 | raise ValueError( 163 | f"Expected a string of length {self.N_qubits}, " 164 | f"but received length {len(string)}." 165 | ) 166 | 167 | labels = self.slice_string(string) 168 | return sum( 169 | self.graph_handler.G[edge[0]][edge[1]].get("weight", 1) 170 | for edge in self.graph_handler.G.edges() 171 | if not self.same_color(labels[edge[0]], labels[edge[1]]) 172 | ) 173 | -------------------------------------------------------------------------------- /qaoa/util/graphutils.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | 3 | 4 | class GraphHandler: 5 | """ 6 | Class that handles graphs. 7 | 8 | Attributes: 9 | num_nodes (int): Number of nodes/vertices in the graph. 10 | num_edges (int): Number of edges in the graph. 11 | G (networkx.Graph): The processed graph with nodes relabeled and maximum degree node at the end. 12 | parallel_edges (dict): Dictionary mapping colors to sets of edges for parallel execution. 13 | """ 14 | 15 | def __init__(self, G): 16 | """ 17 | Initializes the GraphHandler with a given graph. 18 | 19 | Args: 20 | G (networkx.Graph): The input graph to be processed. 21 | 22 | Raises: 23 | Exception: If graph is directed. 24 | Exception: If graph contains nodes with degree less than or equal to 1. 25 | """ 26 | 27 | if isinstance(G, nx.DiGraph): 28 | raise Exception("Graph should be undirected.") 29 | if any(degree <= 1 for node, degree in G.degree()): 30 | print( 31 | "Graph contains nodes with one or zero edges. These can be removed to reduce the size of the problem." 32 | ) 33 | 34 | self.num_nodes = G.number_of_nodes() 35 | self.num_edges = G.number_of_edges() 36 | 37 | # ensure graph has labels 0, 1, ..., num_V-1 38 | G_int = self.__ensure_integer_labels__(G) 39 | # relabel to make node n-1 the one with maximum degree 40 | self.G = self.__get_graph_maxdegree_last_node__(G_int) 41 | # to avoid a deep circuit, we partition the edges into sets which can be executed in parallel 42 | if not nx.is_isomorphic(G, self.G): 43 | raise Exception("Something went wrong.") 44 | 45 | self.__minimum_edge_coloring__() 46 | 47 | def __ensure_integer_labels__(self, G): 48 | """ 49 | Ensures that the nodes of the graph are labeled with integers from 0 to `num_nodes`-1. 50 | 51 | Args: 52 | G (networkx.Graph): The graph to be relabeled. 53 | 54 | Returns: 55 | networkx.Graph: Relabelled graph. 56 | """ 57 | 58 | # Check if nodes are already labeled as 0 to num_nodes-1 59 | if set(G.nodes) == set(range(self.num_nodes)): 60 | return ( 61 | G # Return the graph unchanged if nodes are already labeled correctly 62 | ) 63 | 64 | # If nodes are not labeled correctly, create a mapping to relabel them 65 | node_mapping = {node: i for i, node in enumerate(G.nodes)} 66 | 67 | # Create a new graph with relabeled nodes 68 | H = nx.relabel_nodes(G, node_mapping, copy=True) 69 | 70 | return H 71 | 72 | def __map_colors_to_edges__(self, line_graph_colors, original_graph): 73 | """ 74 | Maps colors from the line graph to edges in the original graph. 75 | 76 | Args: 77 | line_graph_colors (dict): Dictionary mapping edges in the line graph to colors. 78 | original_graph (networkx.Graph): Graph from which the line graph was derived. 79 | 80 | Raises: 81 | ValueError: If the colored edges do not match the edges in the original graph. 82 | 83 | Returns: 84 | dict: Dictionary mapping colors to lists of edges in the original graph. 85 | """ 86 | 87 | # Map colors to edges in the original graph G 88 | color_to_edges = {} 89 | 90 | # Each node in the line graph corresponds to an edge in the original graph G 91 | for edge_in_line_graph, color in line_graph_colors.items(): 92 | original_edge = edge_in_line_graph # This is the corresponding edge in G 93 | if color not in color_to_edges: 94 | color_to_edges[color] = [] 95 | color_to_edges[color].append(original_edge) 96 | 97 | # Perform consistency check 98 | 99 | # Get the set of all edges in G 100 | edges_in_G = set(self.G.edges()) 101 | # Get the set of all edges in color_to_edges 102 | edges_in_coloring = set( 103 | edge for edges in color_to_edges.values() for edge in edges 104 | ) 105 | 106 | if edges_in_G != edges_in_coloring: 107 | raise ValueError( 108 | "The colored edges do not match the edges in the original graph!" 109 | ) 110 | 111 | return color_to_edges 112 | 113 | def __get_graph_maxdegree_last_node__(self, G): 114 | """ 115 | Relabels the nodes of the graph such that the node with the highest degree is at the end (`num_nodes`-1). 116 | 117 | Args: 118 | G (networkx.Graph): The graph to be relabeled. 119 | 120 | Returns: 121 | network.Graph: Relabelled graph. 122 | """ 123 | 124 | # Get node of highest degree 125 | j = sorted(G.degree(), key=lambda x: x[1], reverse=True)[0][0] 126 | if j == self.num_nodes - 1: 127 | return G 128 | else: 129 | # Create a mapping to swap node j and n-1 130 | mapping = {j: self.num_nodes - 1, self.num_nodes - 1: j} 131 | 132 | # Relabel the nodes 133 | H = nx.relabel_nodes(G, mapping, copy=True) 134 | 135 | return H 136 | 137 | def __minimum_edge_coloring__(self, repetitions=100): 138 | """ 139 | Compute an approximate minimum edge coloring of the graph. 140 | 141 | This method applies a greedy vertex coloring algorithm to the line graph of 142 | the original graph `G`, repeated multiple times to minimize the number of 143 | colors. The resulting coloring groups the edges of `G` into parallel sets such that 144 | no two edges in the same group share a vertex. 145 | 146 | This decomposition is useful for minimizing the circuit depth when 147 | implementing diagonal cost Hamiltonians in quantum algorithms. 148 | 149 | Args: 150 | repetitions (int, optional): Number of greedy coloring attempts to perform. More repetitions increase the chance of finding a coloring with fewer colors. 151 | """ 152 | # 153 | # a graph G 154 | # returns minimum edge coloring, i.e., a dict containting the edges for each color 155 | # this can be used to minimize the depth needed to implement diagonal cost Hamiltonians 156 | # example output 157 | # { 3: [(0, 1), (2, 7), (3, 9), (5, 6)], 158 | # 1: [(0, 3), (1, 5), (7, 9)], 159 | # 2: [(0, 9), (1, 6), (2, 5), (3, 8), (4, 7)], 160 | # 0: [(1, 4), (2, 9), (3, 7), (5, 8)] } 161 | # 162 | 163 | # Convert the graph to its line graph 164 | line_G = nx.line_graph(self.G) 165 | 166 | ncolors = self.num_edges + 1 167 | for _ in range(repetitions): 168 | # Apply greedy vertex coloring on the line graph 169 | line_graph_colors = nx.coloring.greedy_color( 170 | line_G, strategy="random_sequential" 171 | ) 172 | 173 | # groups of parallel edges 174 | pe = self.__map_colors_to_edges__(line_graph_colors, self.G) 175 | 176 | num_classes = len(pe) 177 | if num_classes < ncolors: 178 | self.parallel_edges = pe 179 | ncolors = num_classes 180 | -------------------------------------------------------------------------------- /agent/interface.py: -------------------------------------------------------------------------------- 1 | import streamlit as st # Framework for building web applications in Python. 2 | # RUN THIS FILE WITH STREAMLIT: `streamlit run interface.py` 3 | 4 | import re 5 | import matplotlib 6 | matplotlib.use("Agg") # Use non-interactive backend (prevents popups while running in Streamlit). 7 | import matplotlib.pyplot as plt 8 | 9 | # Disable warnings that may show in the Streamlit app. 10 | from PIL import Image 11 | Image.MAX_IMAGE_PIXELS = None 12 | import warnings 13 | warnings.filterwarnings("ignore", category=UserWarning, module="qiskit.visualization.circuit.matplotlib") 14 | 15 | from planner import Planner 16 | 17 | CODE_FENCE_RE = re.compile(r"```(?P[^\n]*)\n(?P.*?)```", re.DOTALL) # Regular expression to match fenced code blocks in markdown. 18 | 19 | def escape_leading_hashes(text: str) -> str: 20 | """Escape leading '#' on lines so they don't render as markdown headers. 21 | Only escapes the first '#' on each line that starts with it. 22 | """ 23 | lines = text.splitlines() 24 | for i, line in enumerate(lines): 25 | stripped = line.lstrip() 26 | # If line starts with '#' after stripping left whitespace, escape it. 27 | if stripped.startswith("#"): 28 | # Preserve leading indentation, escape the first '#'. 29 | prefix_len = len(line) - len(stripped) 30 | lines[i] = line[:prefix_len] + "\\" + line[prefix_len:] 31 | return "\n".join(lines) 32 | 33 | def render_message_content(content: str): 34 | """Render a message that may contain fenced code blocks and markdown.""" 35 | last = 0 36 | for m in CODE_FENCE_RE.finditer(content): 37 | pre = content[last:m.start()] 38 | if pre.strip(): 39 | st.markdown(escape_leading_hashes(pre)) 40 | lang = m.group("lang").strip() or None 41 | code = m.group("code").rstrip("\n") 42 | # Use st.code for code blocks (syntax highlighting supported). 43 | st.code(code, language=lang) 44 | last = m.end() 45 | # Remaining tail. 46 | tail = content[last:] 47 | if tail.strip(): 48 | st.markdown(escape_leading_hashes(tail)) 49 | 50 | # Initialize the planner only once and cache it. 51 | @st.cache_resource 52 | def get_planner(): 53 | return Planner() 54 | planner = get_planner() 55 | 56 | if "messages" not in st.session_state: # Initialize chat messages in session state. 57 | st.session_state.messages = [] 58 | if "current_input" not in st.session_state: # Initialize current input in session state. 59 | st.session_state.current_input = "" 60 | if "pending_response" not in st.session_state: # Initialize pending response flag in session state. 61 | st.session_state.pending_response = False 62 | if "send_clicked" not in st.session_state: # Initialize send button click state in session state. 63 | st.session_state.send_clicked = False 64 | # (Runs once when the app starts). 65 | 66 | st.title("QAOA Agent Chat") # Chat title. 67 | 68 | # If the user has clicked the send button, process the input. 69 | if st.session_state.send_clicked: 70 | query = st.session_state.current_input 71 | if query.strip(): 72 | st.session_state.messages.append({"role": "user", "content": query}) 73 | st.session_state.current_input = "" # Safe: before widget is rendered. Can try commenting this out. 74 | st.session_state.pending_response = True 75 | st.session_state.send_clicked = False 76 | st.rerun() 77 | 78 | # If there is a pending response and messages exist, process the last message. 79 | if st.session_state.pending_response and st.session_state.messages: 80 | with st.spinner("Thinking..."): 81 | result = planner(st.session_state.messages[-1]["content"]) 82 | 83 | captured_media = [] # List to capture media objects (e.g., matplotlib figures, PIL images). 84 | 85 | if "```python" in result: # Run Python code blocks and capture media for rendering in chat. 86 | code = re.search(r"```python\n(.*?)\n```", result, re.DOTALL) # Match Python code blocks. 87 | if code: 88 | code_str = code.group(1) # Extract the code block content. 89 | code_str = re.sub(r"plt\.show\(\)", "", code_str) # Remove plt.show() calls 90 | exec_globals = globals() 91 | exec_locals = {} # Local execution context for the code block. 92 | 93 | try: 94 | matplotlib.use("Agg", force=True) # Force non-interactive backend for matplotlib. 95 | 96 | exec(code_str, exec_globals, exec_locals) # Execute the code block. 97 | seen_figs = set() # Set to track already captured matplotlib figures. 98 | seen_pil = set() # Set to track already captured PIL images. 99 | 100 | # Active pyplot figure(s): 101 | for fig_num in plt.get_fignums(): 102 | fig = plt.figure(fig_num) 103 | if id(fig) not in seen_figs: 104 | captured_media.append(("matplotlib", fig)) 105 | seen_figs.add(id(fig)) 106 | 107 | # Figures/images from exec_locals: 108 | for value in exec_locals.values(): 109 | if hasattr(value, "savefig") and hasattr(value, "add_subplot"): # Matplotlib Figure. 110 | if id(value) not in seen_figs: # Check if figure is already captured. 111 | captured_media.append(("matplotlib", value)) 112 | seen_figs.add(id(value)) 113 | elif isinstance(value, Image.Image): # PIL Image. 114 | if id(value) not in seen_pil: # Check if PIL image is already captured. 115 | captured_media.append(("pil", value)) 116 | seen_pil.add(id(value)) 117 | 118 | except Exception: # Don't crash the app if code execution fails. It this case, the response will just be rendered as usual without output. 119 | pass 120 | 121 | st.session_state.last_media = captured_media # Store captured media in session state for rendering later. 122 | st.session_state.messages.append({ # Append the result to the chat messages. 123 | "role": "assistant", 124 | "content": result, 125 | "media": captured_media 126 | }) 127 | 128 | st.session_state.pending_response = False # Reset pending response flag. 129 | 130 | # Render chat messages. 131 | for idx, msg in enumerate(st.session_state.messages): 132 | with st.chat_message(msg["role"]): 133 | render_message_content(msg["content"]) 134 | # If this is the last assistant message, attach its respective captured media. 135 | # This means that media corresponding to previous messages will not be rendered for better performance. 136 | if msg["role"] == "assistant" and idx == len(st.session_state.messages) - 1: 137 | if "last_media" in st.session_state: 138 | for kind, obj in st.session_state.last_media: 139 | if kind == "matplotlib": 140 | st.pyplot(obj) 141 | plt.close(obj) 142 | elif kind in ("pil", "bytes"): 143 | st.image(obj) 144 | 145 | # User input. 146 | query = st.text_area( 147 | "Your message:", # Label. 148 | height=150, 149 | placeholder="Ask me about QAOA...", 150 | value= st.session_state.current_input, 151 | key="current_input", 152 | label_visibility="collapsed" # Hide unnecessary "Your message:" label visually. 153 | ) 154 | 155 | # Send button to submit the input. 156 | if st.button("Send"): 157 | st.session_state.send_clicked = True 158 | st.rerun() -------------------------------------------------------------------------------- /examples/MaxCut/data/w_ba_n21_k4_0.gml: -------------------------------------------------------------------------------- 1 | graph [ 2 | node [ 3 | id 0 4 | label "0" 5 | ] 6 | node [ 7 | id 1 8 | label "1" 9 | ] 10 | node [ 11 | id 2 12 | label "2" 13 | ] 14 | node [ 15 | id 3 16 | label "3" 17 | ] 18 | node [ 19 | id 4 20 | label "4" 21 | ] 22 | node [ 23 | id 5 24 | label "5" 25 | ] 26 | node [ 27 | id 6 28 | label "6" 29 | ] 30 | node [ 31 | id 7 32 | label "7" 33 | ] 34 | node [ 35 | id 8 36 | label "8" 37 | ] 38 | node [ 39 | id 9 40 | label "9" 41 | ] 42 | node [ 43 | id 10 44 | label "10" 45 | ] 46 | node [ 47 | id 11 48 | label "11" 49 | ] 50 | node [ 51 | id 12 52 | label "12" 53 | ] 54 | node [ 55 | id 13 56 | label "13" 57 | ] 58 | node [ 59 | id 14 60 | label "14" 61 | ] 62 | node [ 63 | id 15 64 | label "15" 65 | ] 66 | node [ 67 | id 16 68 | label "16" 69 | ] 70 | node [ 71 | id 17 72 | label "17" 73 | ] 74 | node [ 75 | id 18 76 | label "18" 77 | ] 78 | node [ 79 | id 19 80 | label "19" 81 | ] 82 | node [ 83 | id 20 84 | label "20" 85 | ] 86 | edge [ 87 | source 0 88 | target 4 89 | weight 0.734722351505153 90 | ] 91 | edge [ 92 | source 0 93 | target 5 94 | weight 0.1779036405477007 95 | ] 96 | edge [ 97 | source 0 98 | target 6 99 | weight 0.6718377137576635 100 | ] 101 | edge [ 102 | source 0 103 | target 14 104 | weight 0.3145253370995559 105 | ] 106 | edge [ 107 | source 1 108 | target 4 109 | weight 0.07844034984827308 110 | ] 111 | edge [ 112 | source 1 113 | target 7 114 | weight 0.7105358527854021 115 | ] 116 | edge [ 117 | source 1 118 | target 11 119 | weight 0.9425579931989997 120 | ] 121 | edge [ 122 | source 1 123 | target 14 124 | weight 0.47618479549149006 125 | ] 126 | edge [ 127 | source 2 128 | target 4 129 | weight 0.8719924035779774 130 | ] 131 | edge [ 132 | source 2 133 | target 5 134 | weight 0.15769612071638694 135 | ] 136 | edge [ 137 | source 2 138 | target 6 139 | weight 0.2608994985518428 140 | ] 141 | edge [ 142 | source 2 143 | target 7 144 | weight 0.9628813941155516 145 | ] 146 | edge [ 147 | source 2 148 | target 8 149 | weight 0.4121474114887961 150 | ] 151 | edge [ 152 | source 2 153 | target 9 154 | weight 0.66874626885296 155 | ] 156 | edge [ 157 | source 2 158 | target 11 159 | weight 0.5547613553987607 160 | ] 161 | edge [ 162 | source 2 163 | target 12 164 | weight 0.9891085573163731 165 | ] 166 | edge [ 167 | source 2 168 | target 13 169 | weight 0.04343964236568809 170 | ] 171 | edge [ 172 | source 3 173 | target 4 174 | weight 0.911166838701471 175 | ] 176 | edge [ 177 | source 3 178 | target 5 179 | weight 0.6714313633020091 180 | ] 181 | edge [ 182 | source 4 183 | target 5 184 | weight 0.45396085676891607 185 | ] 186 | edge [ 187 | source 4 188 | target 6 189 | weight 0.11843338740083653 190 | ] 191 | edge [ 192 | source 4 193 | target 7 194 | weight 0.9754204093852473 195 | ] 196 | edge [ 197 | source 4 198 | target 8 199 | weight 0.274323090061093 200 | ] 201 | edge [ 202 | source 4 203 | target 9 204 | weight 0.48512592254990006 205 | ] 206 | edge [ 207 | source 4 208 | target 10 209 | weight 0.43885673323012564 210 | ] 211 | edge [ 212 | source 4 213 | target 12 214 | weight 0.19348140317173734 215 | ] 216 | edge [ 217 | source 4 218 | target 13 219 | weight 0.4735261396694844 220 | ] 221 | edge [ 222 | source 4 223 | target 14 224 | weight 0.3090622461623441 225 | ] 226 | edge [ 227 | source 4 228 | target 16 229 | weight 0.7683531832964069 230 | ] 231 | edge [ 232 | source 4 233 | target 19 234 | weight 0.1520634638911016 235 | ] 236 | edge [ 237 | source 4 238 | target 20 239 | weight 0.5772580102238359 240 | ] 241 | edge [ 242 | source 5 243 | target 6 244 | weight 0.7644597578327412 245 | ] 246 | edge [ 247 | source 5 248 | target 8 249 | weight 0.35850229293152425 250 | ] 251 | edge [ 252 | source 5 253 | target 9 254 | weight 0.93792085752119 255 | ] 256 | edge [ 257 | source 5 258 | target 10 259 | weight 0.9838734262040565 260 | ] 261 | edge [ 262 | source 5 263 | target 15 264 | weight 0.17933729463416515 265 | ] 266 | edge [ 267 | source 5 268 | target 19 269 | weight 0.2997107022080846 270 | ] 271 | edge [ 272 | source 6 273 | target 7 274 | weight 0.34618397367056375 275 | ] 276 | edge [ 277 | source 6 278 | target 9 279 | weight 0.1754326980227493 280 | ] 281 | edge [ 282 | source 6 283 | target 15 284 | weight 0.17103789068824415 285 | ] 286 | edge [ 287 | source 6 288 | target 16 289 | weight 0.7598564663125048 290 | ] 291 | edge [ 292 | source 6 293 | target 20 294 | weight 0.28466254654726564 295 | ] 296 | edge [ 297 | source 7 298 | target 8 299 | weight 0.09851841049809695 300 | ] 301 | edge [ 302 | source 7 303 | target 12 304 | weight 0.40186954211227655 305 | ] 306 | edge [ 307 | source 7 308 | target 13 309 | weight 0.5179579803154635 310 | ] 311 | edge [ 312 | source 7 313 | target 15 314 | weight 0.49692737936774667 315 | ] 316 | edge [ 317 | source 7 318 | target 18 319 | weight 0.5432725115666631 320 | ] 321 | edge [ 322 | source 8 323 | target 10 324 | weight 0.13101952266800931 325 | ] 326 | edge [ 327 | source 8 328 | target 11 329 | weight 0.6398010432776582 330 | ] 331 | edge [ 332 | source 8 333 | target 14 334 | weight 0.041492130473876676 335 | ] 336 | edge [ 337 | source 8 338 | target 17 339 | weight 0.5266976503654233 340 | ] 341 | edge [ 342 | source 9 343 | target 10 344 | weight 0.5105990500297237 345 | ] 346 | edge [ 347 | source 9 348 | target 13 349 | weight 0.8210532035534311 350 | ] 351 | edge [ 352 | source 9 353 | target 17 354 | weight 0.5745510110446512 355 | ] 356 | edge [ 357 | source 9 358 | target 19 359 | weight 0.9872678097576019 360 | ] 361 | edge [ 362 | source 10 363 | target 11 364 | weight 0.06345906682675329 365 | ] 366 | edge [ 367 | source 10 368 | target 12 369 | weight 0.1936088284481825 370 | ] 371 | edge [ 372 | source 10 373 | target 16 374 | weight 0.5245501131400708 375 | ] 376 | edge [ 377 | source 10 378 | target 17 379 | weight 0.3985651449980899 380 | ] 381 | edge [ 382 | source 11 383 | target 16 384 | weight 0.687516599377126 385 | ] 386 | edge [ 387 | source 11 388 | target 18 389 | weight 0.3239365806681166 390 | ] 391 | edge [ 392 | source 12 393 | target 18 394 | weight 0.39329299824811836 395 | ] 396 | edge [ 397 | source 12 398 | target 20 399 | weight 0.41514559868224965 400 | ] 401 | edge [ 402 | source 14 403 | target 15 404 | weight 0.5032925800926605 405 | ] 406 | edge [ 407 | source 16 408 | target 17 409 | weight 0.6123620174714905 410 | ] 411 | edge [ 412 | source 16 413 | target 20 414 | weight 0.09576558901865162 415 | ] 416 | edge [ 417 | source 17 418 | target 18 419 | weight 0.2097565382032064 420 | ] 421 | edge [ 422 | source 18 423 | target 19 424 | weight 0.6519108083898111 425 | ] 426 | ] 427 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QAOA 2 | 3 | This package is a flexible python implementation of the [Quantum Approximate Optimization Algorithm](https://arxiv.org/pdf/1411.4028.pdf) /[Quantum Alternating Operator ansatz](https://arxiv.org/pdf/1709.03489.pdf) (QAOA) **aimed at researchers** to readily test the performance of a new ansatz, a new classical optimizer, etc. By default it uses qiskit as a backend. 4 | 5 | Install with `pip install qaoa` or `pip install -e .`. 6 | 7 | *** 8 | ### Background 9 | Given a **cost function** 10 | $$c: \lbrace 0, 1\rbrace^n \rightarrow \mathbb{R}$$ 11 | one defines a **problem Hamiltonian** $H_P$ through the action on computational basis states via 12 | 13 | $$ H_P |x\rangle = c(x) |x\rangle,$$ 14 | 15 | which means that ground states minimize the cost function $c$. 16 | Given a parametrized ansatz $| \gamma, \beta \rangle$, a classical optimizer is used to minimize the energy 17 | 18 | $$ \langle \gamma, \beta | H_P | \gamma, \beta \rangle.$$ 19 | 20 | QAOA of depth $p$ consist of the following **ansatz**: 21 | 22 | $$ |\gamma, \beta \rangle = \prod_{l=1}^p \left( U_M(\beta_l) U_P(\gamma_l)\right) | s\rangle, $$ 23 | 24 | where 25 | 26 | - $U_P$ is a family of **phase**-separating operators, 27 | - $U_M$ is a family of **mixing** operators, and 28 | - $|s\rangle$ is a "simple" **initial** state. 29 | 30 | In plain vanilla QAOA these have the form 31 | $U_M(\beta_l)=e^{-i\beta_l X^{\otimes n}}$, $U_P(\gamma_l)=e^{-i\gamma_l H_P}$, and the uniform superposition $| s \rangle = |+\rangle^{\otimes n}$ as initial state. 32 | 33 | *** 34 | ### Create a custom ansatz 35 | 36 | In order to create a custom QAOA ansatz, one needs to specify a [problem](qaoa/problems/base_problem.py), a [mixer](qaoa/mixers/base_mixer.py), and an [initial state](qaoa/initialstates/base_initialstate.py). These base classes have an abstract method `def create_circuit:`which needs to be implemented. The problem base class additionally has an abstract method `def cost:`. 37 | 38 | This library already contains several standard implementations. 39 | 40 | - The following [problem](qaoa/problems/base_problem.py) cases are already available: 41 | - [Max k-CUT binary power of two](qaoa/problems/maxkcut_binary_powertwo.py) * 42 | - [Max k-CUT binary full H](qaoa/problems/maxkcut_binary_fullH.py) 43 | - [Max k-CUT binary one hot](qaoa/problems/maxkcut_binary_one_hot.py) 44 | - [QUBO](qaoa/problems/qubo_problem.py) 45 | - [Exact cover](qaoa/problems/exactcover_problem.py) 46 | - [Portfolio](qaoa/problems/portfolio_problem.py) 47 | - [Graph](qaoa/problems/graph_problem.py) 48 | - The following [mixer](qaoa/mixers/base_mixer.py) cases are already available: 49 | - [X-mixer](qaoa/mixers/x_mixer.py) 50 | - [XY-mixer](qaoa/mixers/xy_mixer.py) 51 | - [Grover-mixer](qaoa/mixers/grover_mixer.py) 52 | - [Max k-CUT grover](qaoa/mixers/maxkcut_grover_mixer.py) 53 | - [Max k-CUT LX](qaoa/mixers/maxkcut_lx_mixer.py) 54 | - The following [initial state](qaoa/initialstates/base_initialstate.py) cases are already available: 55 | - [Plus](qaoa/initialstates/plus_initialstate.py) 56 | - [Statevector](qaoa/initialstates/statevector_initialstate.py) 57 | - [Dicke](qaoa/initialstates/dicke_initialstate.py) 58 | - [Dicke 1- and 2-states superposition](qaoa/initialstates/dicke1_2_initialstate.py) 59 | - [Less than k](qaoa/initialstates/lessthank_initialstate.py) 60 | - [Max k-CUT feasible](qaoa/initialstates/maxkcut_feasible_initialstate.py) 61 | 62 | It is **very easy to extend this list** by providing an implementation of a circuit/cost of the base classes mentioned above. Feel free to fork the repo and create a pull request :-) 63 | 64 | To make an ansatz for the MaxCut problem, the X-mixer and the initial state $|+\rangle^{\otimes n}$ one can create an instance like this: 65 | 66 | qaoa = QAOA( 67 | initialstate=initialstates.Plus(), 68 | problem=problems.MaxKCutBinaryPowerOfTwo(G="some networkx instance", k_cuts=2), 69 | mixer=mixers.X() 70 | ) 71 | 72 | *(can be used for the standard MaxCut with argument k_cuts=2) 73 | *** 74 | ### Run optimization at depth $p$ 75 | 76 | For depth $p=1$ the expectation value can be sampled on an $n\times m$ Cartesian grid over the domain $[0,\gamma_\text{max}]\times[0,\beta_\text{max}]$ with: 77 | 78 | qaoa.sample_cost_landscape() 79 | 80 | ![Energy landscape](images/E.png "Energy landscape") 81 | 82 | Sampling high-dimensional target functions quickly becomes intractable for depth $p>1$. We therefore **iteratively increase the depth**. At each depth a **local optimization** algorithm, e.g. COBYLA, is used to find a local minimum. As **initial guess** the following is used: 83 | 84 | - At depth $p=1$ initial parameters $(\gamma, \beta)$ are given by the lowest value of the sampled cost landscape. 85 | - At depth $p>1$ initial parameters $(\gamma, \beta)$ are based on an [interpolation-based heuristic](https://arxiv.org/pdf/1812.01041.pdf) of the optimal values at the previous depth. 86 | 87 | Running this iterative local optimization to depth $p$ can be done by the following call: 88 | 89 | qaoa.optimize(depth=p) 90 | 91 | The function will call `sample_cost_landscape` if not already done, before iteratively increasing the depth. 92 | 93 | *** 94 | ### Further parameters 95 | 96 | QAOA supports the following keywords: 97 | 98 | qaoa = QAOA( ..., 99 | backend= , 100 | noisemodel= , 101 | optimizer= , 102 | precision= , 103 | shots= , 104 | cvar= 105 | ) 106 | 107 | - `backend`: the backend to be used, defaults to `Aer.get_backend("qasm_simulator")` 108 | - `noisemodel`: the noise model to be used, default to `None`, 109 | - `optimizer`: a list of the optimizer to be used from qiskit-algorithms together with options, defaults to `[COBYLA, {}]`, 110 | - `precision`: sampel until a certain precision of the expectation value is reached based on $\text{error}=\frac{\text{variance}}{\sqrt{\text{shots}}}$, defaults to `None`, 111 | - `shots`: number of shots to be used, defaults to `1024`, 112 | - `cvar`: the value for [conditional value at risk (CVAR)](https://arxiv.org/pdf/1907.04769.pdf), defaults to `1`, which are the standard moments. 113 | 114 | *** 115 | ### Extract results 116 | 117 | Once `qaoa.optimize(depth=p)` is run, one can extract, the expectation value, variance, and parametres for each depth $1\leq i \leq p$ by respectively calling: 118 | 119 | qaoa.get_Exp(depth=i) 120 | qaoa.get_Var(depth=i) 121 | qaoa.get_gamma(depth=i) 122 | qaoa.get_beta(depth=i) 123 | 124 | Additionally, for each depth every time the loss function is called, the **angles, expectation value, variance, maximum cost, minimum cost, **and** number of shots** are stored in 125 | 126 | qaoa.optimization_results[i] 127 | 128 | *** 129 | ### Example use cases 130 | 131 | See [examples here](examples/). 132 | 133 | 134 | *** 135 | ### Minimizing depth of phase separating operator 136 | 137 | Assuming all-to-all connectivity of qubits, one can minimize the depth of the circuit of the phase separating operator by solving the problem of minimum edge colouring. This is implemtend in [GraphHandler](qaoa/util/graphutils.py) and gets automatically invoked. An [example](examples/MaxCut/MinimalDepth.ipynb) output is this 138 | 139 | ![this graph](images/minimal_depth.png "Edge Coloring") 140 | 141 | 142 | *** 143 | ### Tensorize mixers 144 | To tensorize a mixer, i.e. decomposing the mixer into a tensor product of unitaries that is 145 | performed on each qubit, one can call the tensor class with the arguments of mixer and number of qubits in subpart. 146 | 147 | For example, for the standard MaxCut problem above where the X mixer was used, one could find the tensor by writing: 148 | 149 | tensorized_mixer = Tensor(mixer.X(), number_of_qubits_of_subpart) 150 | 151 | 152 | *** 153 | ### Talk to an agent 154 | In the code, there is also included an "agent" folder, which has implemented a specialized QAOA agent. It is possible to ask the agent for either code that implements, or explanations about, the QAOA package. 155 | 156 | There are two ways to run the agents. Either one can run the agent in the terminal, or run an interface. 157 | 158 | To run the agent directly from the terminal, one can simply open the agent folder and run the file planner.py. It is then possible to ask it questions as inputs from the terminal. 159 | 160 | To run the interface, open the agent folder, and run streamlit by typing: streamlit run interface.py 161 | The following window will pop up: 162 | 163 | QAOA Agent interface startup page 164 |

165 | It is then possible to ask questions in the text box. An example could be: 166 | QAOA Agent interface with question page 167 | 168 | For the agent application the following dependencies are needed: langchain, langchain_community, langchain_chroma, langchain_openai and optionally streamlit (for the interface). Additionally, an API-KEY is needed. 169 | 170 | 171 | *** 172 | ### Acknowledgement 173 | We would like to thank for funding of the work by the Research Council of Norway through project number 33202. 174 | -------------------------------------------------------------------------------- /agent/planner.py: -------------------------------------------------------------------------------- 1 | # ----- Imports ----- 2 | from langchain.prompts import PromptTemplate 3 | from langchain.chains import LLMChain 4 | from langchain.memory import ConversationSummaryBufferMemory 5 | from langchain.chat_models import init_chat_model 6 | 7 | # ----- Helper imports ----- 8 | from explainer import Explainer 9 | from coder import Coder 10 | 11 | class Planner: 12 | """ 13 | A planning agent that generates plans for QAOA code or explanations based on user input. It then sends the plan to either the Explainer or Coder agent for further processing. 14 | 15 | Attributes: 16 | model (str): The language model to use for planning. Default is "gpt-4.1". 17 | temperature (float): The temperature for the language model. Default is 0. 18 | 19 | Methods: 20 | __init__(model, temperature): Initializes the planner with an LLM and a planning prompt. 21 | plan(description): Generates a plan based on the user description and stored context. 22 | __call__(description): Calls the planner with a description. 23 | set_context(): Sets or updates the context variable with relevant documentation. 24 | """ 25 | 26 | def __init__(self, model="openai:gpt-4.1", temperature=0): 27 | """Initialize the planning agent with an LLM, a planning prompt, and optional context. 28 | 29 | Args: 30 | model (str): The language model to use for planning. Default is "openai:gpt-4.1". 31 | temperature (float): The temperature for the language model. Default is 0. 32 | """ 33 | # Initialize the language model 34 | self.llm = init_chat_model(model, temperature=temperature) 35 | self.memory = ConversationSummaryBufferMemory( 36 | llm=self.llm, 37 | memory_key="chat_history", 38 | input_key="question", 39 | return_messages=True, 40 | max_token_limit=1000, 41 | ) 42 | self.context = "" 43 | self.set_context() 44 | self.prompt = PromptTemplate( 45 | input_variables=["question", "context", "chat_history"], 46 | template=""" 47 | You are an expert in the QAOA Python package and a code assistant to the USER. Your task is to create a plan (a prompt) for another AGENT to follow. 48 | 49 | You have access to the following context: {context}, which contains the valid options for the QAOA package. The context is divided into three parts: 50 | 1. Valid Initial states 51 | 2. Valid Mixers 52 | 3. Valid Problems 53 | 54 | You have also access to the chat history: {chat_history} 55 | 56 | Given the USER's input: "{question}": 57 | 58 | ***** RULES ***** 59 | ONLY do 1 of the following 3 cases: 60 | --- CASE 0: Validate USER input with the context. 61 | - If any USER-specified initial state, problem, or mixer is NOT an exact match with an option in {context}. Then: 62 | - Respond ONLY with: "The [initial state/problem/mixer] '[name]' is not a valid option based on the documentation." and the list of valid options for the QAOA package provided by the context. 63 | - Do NOT generate anything else, no code, no plans, no titles, no other explanations. 64 | - You will not use a template for this case, but rather a direct response. 65 | - Else, continue to the next case. 66 | 67 | Example of CASE 0: 68 | - USER input: "could you make a qaoa example with the sanne problem, the x mixer and the plus initial state?" 69 | - Context: {context} 70 | - Response: "The problem 'sanne' is not a valid option based on the documentation." 71 | 72 | --- CASE 1: Generate a plan to create code using the QAOA package. 73 | 1. If the USER requests specific code or how to implement QAOA, generate a numbered list of concise, implementation-focused steps to create a Python script using only the QAOA package and only valid intial states/mixers/problems which are explicitly written in the context. 74 | - The title above the steps are ALWAYS "CASE 1: Plan to generate code using the QAOA package". 75 | - Do NOT write any code or call any tools. 76 | - Only describe how to do each step. 77 | 78 | The step template you will use if CASE 1 is selected: 79 | 1. Answer this query: {question} 80 | 81 | - ALWAYS include the steps 1 if CASE 1 applies. 82 | - IF the USER asks for a visualization, include a step that asks for this. If the USER asks for something specific to the visualization, include a step that asks for this. 83 | - IF the USER asks for a cost landscape, include a step that asks for this. 84 | 85 | --- CASE 2: Generate a plan to explain the QAOA package. 86 | 2. If the USER asks for an explanation about the QAOA package, generate a concise TWO-WORD list of components. It should be written as bullet points. 87 | - Title of the list is either "CASE 2: Plan over which components of the QAOA package to explain" or "CASE 2: Plan for explanation of structures and relationships in the QAOA package". 88 | - Use this case if: "explain", "explanation", "components", "parts", or "structure" is in the description and "code" or "implementation" is not. 89 | - Include only the components that are relevant to the USER's request. 90 | 91 | Some the components that can be relevant: 92 | - QAOA class 93 | - Problems classes 94 | - Mixers classes 95 | - Initial states classes 96 | 97 | If the USER asks for a "structure", "relation", "relationship", "overview" or "hierarchy" of the QAOA package then it can be relevant to include: 98 | - structure 99 | - relation 100 | - overview 101 | 102 | - IF the USER ONLY asks for a specific class, method, or variable, then ONLY include that class, method, or variable in the list you generate. It does not need to be in the context to be included. 103 | 104 | Strictly follow these rules: 105 | - Output ONLY the numbered list of steps or the list of components. 106 | - No additional commentary, no code, no explanations, no tool calls. 107 | - If you cannot follow these rules, respond with: "Cannot provide a plan based on the input." 108 | 109 | Remember: Be concise, focused, and precise. 110 | """, 111 | ) 112 | self.chain = LLMChain(llm=self.llm, prompt=self.prompt, memory=self.memory) 113 | 114 | # Initialize the other agents Explainer and Coder with context files 115 | self.other_memory = ConversationSummaryBufferMemory( 116 | llm=self.llm, 117 | memory_key="chat_history", 118 | input_key="question", 119 | return_messages=True, 120 | max_token_limit=1000, 121 | ) 122 | 123 | # Initialize the Explainer and Coder with the same memory 124 | self.explainer = Explainer(self.other_memory, embedding=True) 125 | self.coder = Coder(self.other_memory) 126 | 127 | def plan(self, description: str) -> str: 128 | """Generate a plan based on the user description and stored context.""" 129 | result = self.chain.invoke( 130 | {"question": description, "context": self.context} 131 | ) 132 | # Extract the plan from the result 133 | plan = result["text"] 134 | low_plan = plan.lower() 135 | 136 | # The next query to send to the Explainer or Coder 137 | next_query = "Input from USER: " + description + "\n\nPlan:\n" + plan 138 | 139 | # Print the plan for debugging and better control 140 | print("Plan generated:", plan) 141 | try: 142 | # Check for specific cases in the response to determine whether to use an agent (or not), if agent then which agent to use 143 | if ( 144 | "case 2" in low_plan 145 | or "plan for explanation" in low_plan 146 | or "plan over which components" in low_plan 147 | ): 148 | print("using the Explainer agent") 149 | 150 | # Use the Explainer agent to explain the QAOA package 151 | response = self.explainer.explain(next_query) 152 | elif "case 0" in low_plan or "not a valid option" in low_plan: 153 | # If the plan indicates an invalid option, return a direct response 154 | print("not using an agent, returning response directly") 155 | response = plan 156 | else: 157 | # Use the Coder agent to generate code based on the plan 158 | print("using the Coder agent") 159 | response = self.coder.generate_and_test_code(next_query) 160 | # print("Memory buffer:", self.memory.buffer) 161 | return response 162 | except Exception as e: 163 | print("Error during planning:", e) 164 | raise 165 | 166 | def __call__(self, description: str) -> str: 167 | """Call the planner with a description.""" 168 | return self.plan(description) 169 | 170 | def set_context(self): 171 | """Set or update the context variable.""" 172 | 173 | # Load context from a file listing valid initial states, problems, and mixers 174 | filepath = "valid_initialstates_problems_mixers.txt" 175 | try: 176 | with open(filepath, "r", encoding="utf-8") as f: 177 | self.context = f.read() 178 | except FileNotFoundError: 179 | self.context = "No context available." 180 | 181 | # Run planner.py to start the interaction if an interface is not wanted. 182 | if __name__ == "__main__": 183 | planner = Planner() 184 | while True: 185 | query = input("Ask a question (or 'exit' to quit): ") 186 | if query.lower() in ["exit", "quit"]: 187 | print("Goodbye!") 188 | break 189 | 190 | result = planner(query) 191 | print("\nAnswer:") 192 | print(result) 193 | print("-" * 40) 194 | -------------------------------------------------------------------------------- /qaoa/problems/maxkcut_binary_powertwo.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import numpy as np 3 | import itertools 4 | from qiskit import QuantumCircuit, QuantumRegister, AncillaRegister 5 | from qiskit.circuit import Parameter 6 | from qiskit.circuit.library import PhaseGate 7 | 8 | from qiskit.circuit.library import PauliEvolutionGate 9 | 10 | from qiskit.quantum_info import SparsePauliOp, Pauli 11 | 12 | from .graph_problem import GraphProblem 13 | 14 | 15 | class MaxKCutBinaryPowerOfTwo(GraphProblem): 16 | """ 17 | Max k-CUT binary power of two graph problem. 18 | 19 | Subclass of the `GraphProblem` class. This class uses binary encoding for the problem when k is a power of two, 20 | or when k has been rounded up to the nearest power of two. 21 | 22 | Attributes: 23 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 24 | k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two). 25 | method (str): The method used for circuit construction ("PauliBasis" or "Diffusion"). 26 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 27 | 28 | Methods: 29 | is_power_of_two(k): Checks if the given integer k is a power of two. 30 | validate_parameters(k, method): Validates the input parameters for k and method. 31 | construct_colors(): Constructs the mapping from binary strings to color classes based on k. 32 | create_edge_circuit(theta): Creates the parameterized quantum circuit for an edge, according to the chosen method. 33 | create_edge_circuit_fixed_node(theta): Creates the parameterized quantum circuit for an edge when one node is fixed. 34 | getPauliOperator(k_cuts, color_encoding): Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 35 | 36 | """ 37 | def __init__( 38 | self, 39 | G: nx.Graph, 40 | k_cuts: int, 41 | method: str = "Diffusion", 42 | fix_one_node: bool = False, # this fixes the last node to color 1, i.e., one qubit gets removed 43 | ) -> None: 44 | """ 45 | Args: 46 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 47 | k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two). 48 | method (str): The method used for circuit construction ("PauliBasis" or "Diffusion"). 49 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 50 | 51 | Raises: 52 | ValueError: If k_cuts is not a power of two, is less than 2, greater than 8, or if method is not valid. 53 | """ 54 | MaxKCutBinaryPowerOfTwo.validate_parameters(k_cuts, method) 55 | 56 | self.k_cuts = k_cuts 57 | self.method = method 58 | 59 | N_qubits_per_node = int(np.ceil(np.log2(self.k_cuts))) 60 | super().__init__(G, N_qubits_per_node, fix_one_node) 61 | 62 | if self.method == "PauliBasis": 63 | self.op, self.ophalf = self.getPauliOperator(self.k_cuts, "all") 64 | 65 | self.construct_colors() 66 | 67 | @staticmethod 68 | def is_power_of_two(k) -> bool: 69 | """ 70 | Checks if the given integer k is a power of two. 71 | 72 | Args: 73 | k (int): The integer to check. 74 | 75 | Returns: 76 | bool: True if k is a power of two, False otherwise. 77 | """ 78 | if k > 0 and (k & (k - 1)) == 0: 79 | return True 80 | return False 81 | 82 | @staticmethod 83 | def validate_parameters(k, method) -> None: 84 | """ 85 | Validates the input parameters for k and method. 86 | 87 | Args: 88 | k (int): Number of partitions (colors). 89 | method (str): Circuit construction method ("PauliBasis" or "Diffusion"). 90 | 91 | Raises: 92 | ValueError: If k is not a power of two. 93 | ValueError: If k is less than 2 or greater than 8. 94 | ValueError: If method is not valid. 95 | """ 96 | ### 1) k_cuts must be a power of 2 97 | if not MaxKCutBinaryPowerOfTwo.is_power_of_two(k): 98 | raise ValueError("k_cuts must be a power of two") 99 | 100 | ### 2) k_cuts needs to be between 2 and 8 101 | if (k < 2) or (k > 8): 102 | raise ValueError( 103 | "k_cuts must be 2 or more, and is not implemented for k_cuts > 8" 104 | ) 105 | 106 | ### 3) method 107 | valid_methods = ["PauliBasis", "Diffusion"] 108 | if method not in valid_methods: 109 | raise ValueError("method must be in " + str(valid_methods)) 110 | 111 | def construct_colors(self): 112 | """ 113 | Constructs the mapping from binary strings to color classes based on k. 114 | 115 | Raises: 116 | ValueError: If k_cuts is not supported. 117 | """ 118 | if self.k_cuts == 2: 119 | self.colors = {"color1": ["0"], "color2": ["1"]} 120 | elif self.k_cuts == 4: 121 | self.colors = { 122 | "color1": ["00"], 123 | "color2": ["01"], 124 | "color3": ["10"], 125 | "color4": ["11"], 126 | } 127 | elif self.k_cuts == 8: 128 | self.colors = { 129 | "color1": ["000"], 130 | "color2": ["001"], 131 | "color3": ["010"], 132 | "color4": ["011"], 133 | "color5": ["100"], 134 | "color6": ["101"], 135 | "color7": ["110"], 136 | "color8": ["111"], 137 | } 138 | # Create a dictionary to map each index to its corresponding set 139 | self.bitstring_to_color = {} 140 | for key, indices in self.colors.items(): 141 | for index in indices: 142 | self.bitstring_to_color[index] = key 143 | 144 | def create_edge_circuit(self, theta): 145 | """ 146 | Creates the parameterized quantum circuit for an edge, according to the chosen method. 147 | 148 | Args: 149 | theta (float): The phase parameter. 150 | 151 | Returns: 152 | qc (QuantumCircuit): The constructed quantum circuit for the edge. 153 | """ 154 | qc = QuantumCircuit(2 * self.N_qubits_per_node) 155 | if self.method == "PauliBasis": 156 | qc.append(PauliEvolutionGate(self.op, time=theta), qc.qubits) 157 | else: 158 | for k in range(self.N_qubits_per_node): 159 | qc.cx(k, self.N_qubits_per_node + k) 160 | qc.x(self.N_qubits_per_node + k) 161 | # C^{n-1}Phase 162 | if self.N_qubits_per_node == 1: 163 | phase_gate = PhaseGate(-theta) 164 | else: 165 | phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1) 166 | qc.append( 167 | phase_gate, 168 | [ 169 | self.N_qubits_per_node - 1 + ind 170 | for ind in range(1, self.N_qubits_per_node + 1) 171 | ], 172 | ) 173 | for k in reversed(range(self.N_qubits_per_node)): 174 | qc.x(self.N_qubits_per_node + k) 175 | qc.cx(k, self.N_qubits_per_node + k) 176 | return qc 177 | 178 | def create_edge_circuit_fixed_node(self, theta): 179 | """ 180 | Creates the parameterized quantum circuit for an edge when one node is fixed. 181 | 182 | Args: 183 | theta (float): The phase parameter. 184 | 185 | Returns: 186 | qc (QuantumCircuit): The constructed quantum circuit for the edge with a fixed node. 187 | """ 188 | qc = QuantumCircuit(self.N_qubits_per_node) 189 | if self.method == "PauliBasis": 190 | qc.append(PauliEvolutionGate(self.ophalf, time=-theta), qc.qubits) 191 | else: 192 | qc.x(qc.qubits) 193 | # C^{n-1}Phase 194 | if self.N_qubits_per_node == 1: 195 | phase_gate = PhaseGate(-theta) 196 | else: 197 | phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1) 198 | qc.append(phase_gate, qc.qubits) 199 | qc.x(qc.qubits) 200 | return qc 201 | 202 | def getPauliOperator(self, k_cuts, color_encoding): 203 | """ 204 | Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 205 | 206 | Args: 207 | k_cuts (int): Number of partitions (colors). 208 | color_encoding (str): The encoding scheme for colors. 209 | 210 | Returns: 211 | op (SparsePauliOp): The full Pauli operator for the cost Hamiltonian. 212 | ophalf (SparsePauliOp): The half Pauli operator for the fixed-node case. 213 | """ 214 | # flip Pauli strings, because of qiskit's little endian encoding 215 | if k_cuts == 2: 216 | P = [ 217 | [2 / (2**1), Pauli("ZZ")], 218 | ] 219 | Phalf = [ 220 | [2 / (2**1), Pauli("Z")], 221 | ] 222 | elif k_cuts == 4: 223 | P = [ 224 | [-8 / (2**4), Pauli("IIII"[::-1])], 225 | [+8 / (2**4), Pauli("IZIZ"[::-1])], 226 | [+8 / (2**4), Pauli("ZIZI"[::-1])], 227 | [+8 / (2**4), Pauli("ZZZZ"[::-1])], 228 | ] 229 | Phalf = [ 230 | [-8 / (2**4), Pauli("II"[::-1])], 231 | [+8 / (2**4), Pauli("IZ"[::-1])], 232 | [+8 / (2**4), Pauli("ZI"[::-1])], 233 | [+8 / (2**4), Pauli("ZZ"[::-1])], 234 | ] 235 | else: 236 | P = [ 237 | [-48 / (2**6), Pauli("IIIIII"[::-1])], 238 | [+16 / (2**6), Pauli("IIZIIZ"[::-1])], 239 | [+16 / (2**6), Pauli("IZIIZI"[::-1])], 240 | [+16 / (2**6), Pauli("IZZIZZ"[::-1])], 241 | [+16 / (2**6), Pauli("ZIIZII"[::-1])], 242 | [+16 / (2**6), Pauli("ZIZZIZ"[::-1])], 243 | [+16 / (2**6), Pauli("ZZIZZI"[::-1])], 244 | [+16 / (2**6), Pauli("ZZZZZZ"[::-1])], 245 | ] 246 | Phalf = [ 247 | [-48 / (2**6), Pauli("III"[::-1])], 248 | [+16 / (2**6), Pauli("IIZ"[::-1])], 249 | [+16 / (2**6), Pauli("IZI"[::-1])], 250 | [+16 / (2**6), Pauli("IZZ"[::-1])], 251 | [+16 / (2**6), Pauli("ZII"[::-1])], 252 | [+16 / (2**6), Pauli("ZIZ"[::-1])], 253 | [+16 / (2**6), Pauli("ZZI"[::-1])], 254 | [+16 / (2**6), Pauli("ZZZ"[::-1])], 255 | ] 256 | 257 | # devide coefficients by 2, since: 258 | # "The evolution gates are related to the Pauli rotation gates by a factor of 2" 259 | op = SparsePauliOp([item[1] for item in P], coeffs=[item[0] / 2 for item in P]) 260 | ophalf = SparsePauliOp( 261 | [item[1] for item in Phalf], coeffs=[item[0] / 2 for item in Phalf] 262 | ) 263 | return op, ophalf 264 | -------------------------------------------------------------------------------- /agent/coder.py: -------------------------------------------------------------------------------- 1 | # ----- Load API key ------ 2 | from dotenv import load_dotenv 3 | load_dotenv() 4 | 5 | import re 6 | from pathlib import Path 7 | from typing import Union, Optional 8 | import io 9 | from contextlib import redirect_stdout, redirect_stderr 10 | import matplotlib 11 | 12 | # ----- LangChain imports ----- 13 | from langchain_openai import ChatOpenAI 14 | from langchain.prompts import PromptTemplate 15 | from langchain.memory import ConversationSummaryBufferMemory 16 | from langchain.chains import ConversationalRetrievalChain 17 | 18 | # ----- Helper imports ----- 19 | from saveembedding import SaveEmbedding 20 | 21 | repo_root = Path(__file__).resolve().parent.parent # Go up to QAOA_Sanne root 22 | DEFAULT_CONTEXT = [repo_root / "examples" / "MaxCut" / "KCutExamples.ipynb", repo_root / "qaoa" / "qaoa.py"] 23 | 24 | class Coder: 25 | """ 26 | A coding assistant that generates, executes, and improves Python code based on user queries. 27 | 28 | This class uses a conversational retrieval chain to maintain context and memory of previous interactions. 29 | It can generate code, analyze errors, and improve code based on feedback. 30 | It also captures media outputs from executed code for rendering in a chat interface. 31 | """ 32 | def __init__(self, memory = None, context_files: Optional[list[Union[str, Path]]] = DEFAULT_CONTEXT, model:str="gpt-4o-mini"): 33 | self.llm = ChatOpenAI(model=model, temperature=0) # LLM. 34 | if memory is not None: # If a memory object is provided, use it (for sharing memory between instances). 35 | self.memory = memory 36 | else: # If no memory is provided, create a new memory object. 37 | self.memory = ConversationSummaryBufferMemory( 38 | llm=self.llm, 39 | memory_key="chat_history", # See prompt template for usage 40 | input_key="question", 41 | return_messages=True, 42 | max_token_limit=1000, # Limit memory size to avoid excessive context 43 | ) 44 | # Initialize vector store and retriever for context files (saved locally in embeddings to save tokens). 45 | embedding = SaveEmbedding(context_files, "Coder_embedding", "embeddings/Coder_embedding", "embeddings/Coder_cache") 46 | self.vectorstore = embedding.get_vectorstore() 47 | self.retriever = embedding.get_retriever() 48 | self.context = embedding.get_context() 49 | self._initialize_agent() 50 | 51 | def execute_code(self, code: str) -> str: 52 | """Executes the provided Python code and returns only error messages if any occur.""" 53 | try: 54 | # Remove Markdown code fences if present 55 | code = re.sub(r"^```(?:python)?", "", code.strip(), flags=re.IGNORECASE) 56 | code = re.sub(r"```$", "", code.strip()) 57 | 58 | # Redirect stdout to suppress circuit diagrams 59 | exec_globals = {} 60 | f = io.StringIO() 61 | 62 | with redirect_stdout(f), redirect_stderr(f): 63 | exec(code.strip(), exec_globals) 64 | 65 | # Only return success message if no errors 66 | return "SUCCESS: Code executed without errors" 67 | 68 | except Exception as e: 69 | # Return just the error type and message, not full traceback 70 | return f"ERROR: {type(e).__name__}: {str(e)}" 71 | 72 | def _initialize_agent(self) -> None: 73 | """Initialize and return the agent executor.""" 74 | 75 | code_suggestion_prompt = PromptTemplate( # Prompt!!! 76 | input_variables=["context", "question"], 77 | template="""You are a AI, a Python coding assistant. 78 | 79 | You have four tasks based on the input: 80 | 81 | 1. If asked a query with no error information, generate Python code to solve the task. 82 | 2. If provided with an error message, analyze the error and suggest improvements to the code without generating new code. 83 | 3. If asked to improve code based on suggestions, generate improved Python code considering the provided feedback. 84 | 4. If an explaination is requested, provide a concise explanation. 85 | 86 | Context: 87 | {context} 88 | 89 | Conversation history: 90 | {chat_history} 91 | 92 | Human: {question} 93 | AI: 94 | 95 | Guidelines: 96 | 1. Generate Python code to solve the task provided by the Human. 97 | 2. The code should be complete and executable 98 | 3. The code should include all necessary imports and mainly use the QAOA package. Don't include any imports that are not used. 99 | 4. The code should be formatted in markdown with ```python code fences 100 | 5. The code should include BRIEF comments in the code explaining key steps. 101 | 6. If analyzing an error, provide concise suggestions for improvement without generating code. 102 | 7. If analyzing an error and the error is 'NoneType', the function probably updates an internal variable, rather than returning a value. 103 | In this case, try to find the variable that is updated and suggest using this instead. 104 | 8. Phrase all responses as if it is the first response to its corresponding query. i.e. don't mention executing code or changes made after executing code. 105 | 9. After generating code, briefly explain the approach and any potential limitations in the code or discrepancies between the code and the task. 106 | 10. For parts of the task that are unspecified, provide brief reasoning for your choices. 107 | 11. Refer to previous tasks and responses in the conversation to maintain context and continuity. 108 | """, 109 | ) 110 | # Initialize chain that handles memory 111 | self.qa_chain = ConversationalRetrievalChain.from_llm( 112 | llm=self.llm, 113 | retriever=( 114 | self.vectorstore.as_retriever( 115 | search_type="similarity", search_kwargs={"k": 4} 116 | ) 117 | if self.vectorstore 118 | else None 119 | ), 120 | memory=self.memory, 121 | combine_docs_chain_kwargs={ 122 | "prompt": code_suggestion_prompt, 123 | }, 124 | ) 125 | 126 | # Main function. 127 | def generate_and_test_code(self, query: str, max_iterations: int = 3) -> None: 128 | """Generate code, test it, and improve based on feedback.""" 129 | current_code = None 130 | error_analysis = None 131 | 132 | for iteration in range(max_iterations): # Iterate up to max_iterations. 133 | print(f"\033[90m\n--- Iteration {iteration + 1} ---\033[0m") 134 | 135 | # Generate new or improve old code. 136 | if current_code is None: 137 | print("\033[90m\nGenerating initial response...\033[0m") 138 | result = self.qa_chain.invoke({"question": query}) 139 | else: 140 | print("\033[90m\nImproving response based on previous error...\033[0m") 141 | result = self.qa_chain.invoke( 142 | { 143 | "question": f"Improve this code based on the following feedback: {error_analysis}\nOriginal task: {query}\nCode:\n{self._extract_code_block(current_code)}" 144 | } 145 | ) 146 | 147 | current_code = result["answer"] # Extract response text. 148 | 149 | print("\033[90m\nResponse:\033[0m") 150 | print(f"\033[90m\n{current_code}\033[0m") 151 | 152 | # Execute the code if it contains a code block. 153 | if "```" in current_code: 154 | matplotlib.use( 155 | "Agg" 156 | ) # Use a non-interactive backend for matplotlib (no verbose output in console). 157 | print("\033[96m\nExecuting code...\033[0m") 158 | execution_result = self.execute_code( 159 | self._extract_code_block(current_code) 160 | ) 161 | matplotlib.use("TkAgg") # Reset to default backend 162 | print(f"\033[96m{execution_result}\033[0m") 163 | if "ERROR" in execution_result: 164 | print(f"\033[96m\nGenerating error analysis...\033[0m") 165 | result = self.qa_chain.invoke( 166 | { 167 | "question": f"""Analyze the following error: {execution_result}. 168 | Provide suggestions for improving the code without generating new code.""" 169 | } 170 | ) 171 | error_analysis = result["answer"] 172 | print(f"\033[96mError analysis: {error_analysis}\033[0m") 173 | else: 174 | return current_code # Return the response if no errors occurred. 175 | 176 | else: # If no code block is found, just return the response (nothing to test, we just trust it). 177 | return current_code 178 | 179 | print(f"\nReached maximum iterations ({max_iterations})") 180 | return current_code # Return the last generated response when max tries are reached. 181 | 182 | def _extract_code_block(self, text: str) -> str: 183 | """Extract code from markdown block.""" 184 | match = re.search(r"```python(.*?)```", text, re.DOTALL) 185 | if match: 186 | return match.group(1).strip() 187 | return text.strip() 188 | 189 | 190 | if __name__ == "__main__": 191 | 192 | # Example usage. 193 | context_files = ["./examples/MaxCut/KCutExamples.ipynb", "./qaoa/qaoa.py"] 194 | assistant = Coder(context_files) 195 | 196 | # First query. 197 | query1 = "Create a qaoa instance using onehot encoding." 198 | print("\nFirst query: ") 199 | print(f"\033[1m{query1}\033[0m") 200 | final_code1 = assistant.generate_and_test_code(query1) 201 | print("\nFinal response to first query:") 202 | print(f"\033[1m{final_code1}\033[0m") 203 | 204 | # Second query relies on memory of the first one. 205 | # query2 = "Why did you choose the initial state and mixer like that?" 206 | # print("\nSecond query: ") 207 | # print(f"\033[1m{query2}\033[0m") 208 | # final_response2 = assistant.qa_chain.invoke({"question": query2}) 209 | # print("\nFinal response to second query:") 210 | # print(f"\033[1m{final_response2["answer"]}\033[0m") 211 | 212 | # # Third query. 213 | # query3 = """Create a qaoa circuit solving the max k-cut problem with k = 3 for this 10-node graph using binary encoding and the full hamiltonian: 214 | # graph [ 215 | # node [ 216 | # id 0 217 | # label "0" 218 | # ] 219 | # node [ 220 | # id 1 221 | # label "1" 222 | # ] 223 | # node [ 224 | # id 2 225 | # label "2" 226 | # ] 227 | # node [ 228 | # id 3 229 | # label "3" 230 | # ] 231 | # node [ 232 | # id 4 233 | # label "4" 234 | # ] 235 | # node [ 236 | # id 5 237 | # label "5" 238 | # ] 239 | # node [ 240 | # id 6 241 | # label "6" 242 | # ] 243 | # node [ 244 | # id 7 245 | # label "7" 246 | # ] 247 | # node [ 248 | # id 8 249 | # label "8" 250 | # ] 251 | # node [ 252 | # id 9 253 | # label "9" 254 | # ] 255 | # edge [ 256 | # source 0 257 | # target 4 258 | # weight 0.3246074330296992 259 | # ] 260 | # edge [ 261 | # source 0 262 | # target 7 263 | # weight 0.6719596645247027 264 | # ] 265 | # edge [ 266 | # source 1 267 | # target 4 268 | # weight 0.5033779645445525 269 | # ] 270 | # edge [ 271 | # source 1 272 | # target 5 273 | # weight 0.8197417437657258 274 | # ] 275 | # edge [ 276 | # source 1 277 | # target 6 278 | # weight 0.1689752608979167 279 | # ] 280 | # edge [ 281 | # source 2 282 | # target 4 283 | # weight 0.8578794331926194 284 | # ] 285 | # edge [ 286 | # source 2 287 | # target 5 288 | # weight 0.10889087475274517 289 | # ] 290 | # edge [ 291 | # source 2 292 | # target 6 293 | # weight 0.29609241287667165 294 | # ] 295 | # edge [ 296 | # source 2 297 | # target 7 298 | # weight 0.3385595778596342 299 | # ] 300 | # edge [ 301 | # source 2 302 | # target 9 303 | # weight 0.49871018015134483 304 | # ] 305 | # edge [ 306 | # source 3 307 | # target 4 308 | # weight 0.5646214337732219 309 | # ] 310 | # edge [ 311 | # source 3 312 | # target 5 313 | # weight 0.22675259631935551 314 | # ] 315 | # edge [ 316 | # source 3 317 | # target 6 318 | # weight 0.42653644275637315 319 | # ] 320 | # edge [ 321 | # source 3 322 | # target 8 323 | # weight 0.9458888986056379 324 | # ] 325 | # edge [ 326 | # source 4 327 | # target 5 328 | # weight 0.6274516118216547 329 | # ] 330 | # edge [ 331 | # source 4 332 | # target 7 333 | # weight 0.6461631361850252 334 | # ] 335 | # edge [ 336 | # source 4 337 | # target 8 338 | # weight 0.07077280704236999 339 | # ] 340 | # edge [ 341 | # source 4 342 | # target 9 343 | # weight 0.061962597519110374 344 | # ] 345 | # edge [ 346 | # source 5 347 | # target 6 348 | # weight 0.12115603714424517 349 | # ] 350 | # edge [ 351 | # source 5 352 | # target 7 353 | # weight 0.6596288196387271 354 | # ] 355 | # edge [ 356 | # source 5 357 | # target 8 358 | # weight 0.8184188538157214 359 | # ] 360 | # edge [ 361 | # source 5 362 | # target 9 363 | # weight 0.686461546892179 364 | # ] 365 | # edge [ 366 | # source 7 367 | # target 8 368 | # weight 0.5014423218237379 369 | # ] 370 | # edge [ 371 | # source 8 372 | # target 9 373 | # weight 0.11336603624375363 374 | # ] 375 | # ] 376 | # Visualize both the graph and the circuit.""" 377 | # print("\nThird query: ") 378 | # print(f"\033[1m{query3}\033[0m") 379 | # final_code3 = assistant.generate_and_test_code(query3) 380 | # print("\nFinal response to third query:") 381 | # print(f"\033[1m{final_code3}\033[0m") 382 | 383 | # # Print memory summary 384 | # print("\033[95m\nMemory summary:\033[0m") 385 | # print(f"\033[95m{assistant.memory.buffer}\033[0m") -------------------------------------------------------------------------------- /unittests/test_maxkcut_binary_problem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import networkx as nx 3 | import unittest 4 | 5 | from qiskit import QuantumRegister, QuantumCircuit 6 | from qiskit.quantum_info import Statevector 7 | 8 | import sys 9 | 10 | sys.path.append("../") 11 | from qaoa.problems import MaxKCutBinaryPowerOfTwo, MaxKCutBinaryFullH 12 | 13 | 14 | class TestMaxKCutBinaryProblem(unittest.TestCase): 15 | def __init__(self, methodname): 16 | super().__init__(methodname) 17 | 18 | V = np.arange(0, 2, 1) 19 | E = [(0, 1, 1.0)] 20 | 21 | self.G = nx.Graph() 22 | self.G.add_nodes_from(V) 23 | self.G.add_weighted_edges_from(E) 24 | 25 | def stateVectorToBitstring(self, sv): 26 | probabilities = np.abs(sv) ** 2 27 | # Check if the array has exactly one `1` and the rest are `0`s 28 | is_comp_basis_state = np.count_nonzero(probabilities) == 1 and np.all( 29 | np.logical_or(probabilities == 0, probabilities == 1) 30 | ) 31 | self.assertTrue(is_comp_basis_state) 32 | 33 | # Find the index of the highest probability 34 | max_prob_index = np.argmax(probabilities) 35 | 36 | num_qubits = int(np.log2(len(sv))) 37 | 38 | # Convert index to bitstring (assuming qubits in little-endian order) 39 | bitstring = format(max_prob_index, f"0{num_qubits}b") 40 | return bitstring 41 | 42 | def test_MaxKCutBinaryPowerTwo(self): 43 | for k in [2, 4, 8]: 44 | for method in ["PauliBasis", "Diffusion"]: 45 | problem = MaxKCutBinaryPowerOfTwo( 46 | self.G, 47 | k, 48 | method=method, 49 | ) 50 | problem.create_circuit() 51 | circuit = problem.circuit 52 | 53 | theta = np.pi 54 | circuit.assign_parameters([theta], inplace=True) 55 | # for k = 2 RZ is applied instead of a phase gate 56 | # they are equal up to a global phase, which we retract 57 | if method == "PauliBasis": 58 | circuit.global_phase = -theta / 2 59 | 60 | num_qubits = problem.N_qubits 61 | for i in range(2 ** int(num_qubits / 2)): 62 | for j in range(2 ** int(num_qubits / 2)): 63 | q = QuantumRegister(len(circuit.qubits)) 64 | circuit_with_IX = QuantumCircuit(q) 65 | binary_str_i = format( 66 | i, f"0{int(num_qubits/2)}b" 67 | ) # Create a binary string with leading zeros 68 | binary_str_j = format( 69 | j, f"0{int(num_qubits/2)}b" 70 | ) # Create a binary string with leading zeros 71 | for ind, bit in enumerate(binary_str_i): 72 | if bit == "1": 73 | circuit_with_IX.x( 74 | ind 75 | ) # Apply an X-gate to the corresponding qubit 76 | for ind, bit in enumerate(binary_str_j): 77 | if bit == "1": 78 | circuit_with_IX.x( 79 | int(num_qubits / 2) + ind 80 | ) # Apply an X-gate to the corresponding qubit 81 | 82 | circuit_with_IX_and_Hamiltonian = circuit_with_IX.compose( 83 | circuit, inplace=False 84 | ) 85 | 86 | sv_IX = Statevector(circuit_with_IX).data 87 | sv_IX_Hamiltonian = Statevector( 88 | circuit_with_IX_and_Hamiltonian 89 | ).data 90 | 91 | inner_product = np.vdot(sv_IX, sv_IX_Hamiltonian) 92 | 93 | bitstring = self.stateVectorToBitstring(sv_IX) 94 | 95 | # qiskit binary strings use little endian encoding, but our cost function expects big endian encoding. Therefore, we reverse the order 96 | bitstring = bitstring[::-1] 97 | 98 | # there should be a phase difference, if the nodes have the same color or not 99 | int_i = int(binary_str_i, 2) 100 | int_j = int(binary_str_j, 2) 101 | 102 | if binary_str_i == binary_str_j: 103 | self.assertTrue(np.isclose(inner_product, -1)) 104 | self.assertTrue(np.isclose(problem.cost(bitstring), 0)) 105 | else: 106 | self.assertTrue(np.isclose(inner_product, 1)) 107 | self.assertTrue(np.isclose(problem.cost(bitstring), 1)) 108 | 109 | def test_MaxKCutBinaryPowerTwo_PauliBasisequalDiffusion(self): 110 | # This tests if the Hamiltonians of "method=PauliBasis" is equal to "method=Diffusion" 111 | 112 | theta = -1.92748 113 | 114 | for k in [2, 4, 8]: 115 | statevector = {} 116 | for method in ["PauliBasis", "Diffusion"]: 117 | problem = MaxKCutBinaryPowerOfTwo( 118 | self.G, 119 | k, 120 | method=method, 121 | ) 122 | problem.create_circuit() 123 | circuit = problem.circuit 124 | 125 | circuit.assign_parameters([theta], inplace=True) 126 | # for k = 2 RZ is applied instead of a phase gate 127 | # they are equal up to a global phase, which we retract 128 | if method == "PauliBasis": 129 | circuit.global_phase = -theta / 2 130 | 131 | q = QuantumRegister(len(circuit.qubits)) 132 | circ = QuantumCircuit(q) 133 | 134 | circ.h(q[: problem.N_qubits]) 135 | 136 | circuit = circ.compose(circuit, inplace=False) 137 | 138 | statevector[method] = Statevector(circuit).data 139 | self.assertTrue( 140 | np.allclose( 141 | statevector["PauliBasis"], statevector["Diffusion"], atol=1e-8 142 | ) 143 | ) 144 | 145 | def test_MaxKCutBinaryFullH(self): 146 | for k in [3, 5, 6, 7]: 147 | for method in ["PauliBasis", "Diffusion", "PowerOfTwo"]: 148 | if k in [3, 7]: 149 | colors = [ 150 | "LessThanK", 151 | ] 152 | else: 153 | colors = ["LessThanK", "max_balanced"] 154 | for color_encoding in colors: 155 | problem = MaxKCutBinaryFullH( 156 | self.G, 157 | k, 158 | color_encoding=color_encoding, 159 | method=method, 160 | ) 161 | problem.create_circuit() 162 | circuit = problem.circuit 163 | 164 | theta = np.pi 165 | circuit.assign_parameters([theta], inplace=True) 166 | # for k = 2 RZ is applied instead of a phase gate 167 | # they are equal up to a global phase, which we retract 168 | if method == "PauliBasis": 169 | circuit.global_phase = -theta / 2 170 | 171 | num_qubits = problem.N_qubits 172 | for i in range(2 ** int(num_qubits / 2)): 173 | for j in range(2 ** int(num_qubits / 2)): 174 | q = QuantumRegister(len(circuit.qubits)) 175 | circuit_with_IX = QuantumCircuit(q) 176 | binary_str_i = format( 177 | i, f"0{int(num_qubits/2)}b" 178 | ) # Create a binary string with leading zeros 179 | binary_str_j = format( 180 | j, f"0{int(num_qubits/2)}b" 181 | ) # Create a binary string with leading zeros 182 | for ind, bit in enumerate(binary_str_i): 183 | if bit == "1": 184 | circuit_with_IX.x( 185 | ind 186 | ) # Apply an X-gate to the corresponding qubit 187 | for ind, bit in enumerate(binary_str_j): 188 | if bit == "1": 189 | circuit_with_IX.x( 190 | int(num_qubits / 2) + ind 191 | ) # Apply an X-gate to the corresponding qubit 192 | 193 | circuit_with_IX_and_Hamiltonian = circuit_with_IX.compose( 194 | circuit, inplace=False 195 | ) 196 | 197 | sv_IX = Statevector(circuit_with_IX).data 198 | sv_IX_Hamiltonian = Statevector( 199 | circuit_with_IX_and_Hamiltonian 200 | ).data 201 | 202 | inner_product = np.vdot(sv_IX, sv_IX_Hamiltonian) 203 | 204 | bitstring = self.stateVectorToBitstring(sv_IX) 205 | 206 | # remove ancilla bits 207 | if method == "PowerOfTwo": 208 | bitstring = bitstring[2:] 209 | 210 | # qiskit binary strings use little endian encoding, but our cost function expects big endian encoding. Therefore, we reverse the order 211 | bitstring = bitstring[::-1] 212 | 213 | # there should be a phase difference, if the nodes have the same color or not 214 | int_i = int(binary_str_i, 2) 215 | int_j = int(binary_str_j, 2) 216 | if color_encoding == "LessThanK": 217 | samecolor = (int_i >= k - 1) and (int_j >= k - 1) 218 | elif color_encoding == "max_balanced": 219 | if k == 5: 220 | # ((0,1), (2), (3), (4, 5), (6,7)) 221 | samecolor = ( 222 | ((int_i == 0) and (int_j == 1)) 223 | or ((int_i == 1) and (int_j == 0)) 224 | or ((int_i == 4) and (int_j == 5)) 225 | or ((int_i == 5) and (int_j == 4)) 226 | or ((int_i == 6) and (int_j == 7)) 227 | or ((int_i == 7) and (int_j == 6)) 228 | ) 229 | elif k == 6: 230 | # ((0,1), (2), (3), (4, 5), (6), (7)) 231 | samecolor = ( 232 | ((int_i == 0) and (int_j == 1)) 233 | or ((int_i == 1) and (int_j == 0)) 234 | or ((int_i == 4) and (int_j == 5)) 235 | or ((int_i == 5) and (int_j == 4)) 236 | ) 237 | 238 | if (binary_str_i == binary_str_j) or samecolor: 239 | self.assertTrue(np.isclose(inner_product, -1)) 240 | self.assertTrue(np.isclose(problem.cost(bitstring), 0)) 241 | else: 242 | self.assertTrue(np.isclose(inner_product, 1)) 243 | self.assertTrue(np.isclose(problem.cost(bitstring), 1)) 244 | 245 | def test_MaxKCutBinaryPowerTwo_PauliBasisequalDiffusion(self): 246 | # This tests if the Hamiltonians of "method=PauliBasis" is equal to "method=Diffusion" is equal to "method="PowerOfTwo" 247 | 248 | theta = -1.92748 249 | 250 | for k in [3, 5, 6, 7]: 251 | if k in [3, 7]: 252 | colors = [ 253 | "LessThanK", 254 | ] 255 | else: 256 | colors = ["LessThanK", "max_balanced"] 257 | for color_encoding in colors: 258 | statevector = {} 259 | for method in ["PauliBasis", "Diffusion", "PowerOfTwo"]: 260 | problem = MaxKCutBinaryFullH( 261 | self.G, 262 | k, 263 | color_encoding=color_encoding, 264 | method=method, 265 | ) 266 | 267 | problem.create_circuit() 268 | circuit = problem.circuit 269 | 270 | circuit.assign_parameters([theta], inplace=True) 271 | # for k = 2 RZ is applied instead of a phase gate 272 | # they are equal up to a global phase, which we retract 273 | if method == "PauliBasis": 274 | circuit.global_phase = -theta / 2 275 | 276 | q = QuantumRegister(len(circuit.qubits)) 277 | circ = QuantumCircuit(q) 278 | 279 | circ.h(q[: problem.N_qubits]) 280 | 281 | circuit = circ.compose(circuit, inplace=False) 282 | 283 | if method == "PowerOfTwo": 284 | sv = Statevector(circuit).data 285 | # Reshape the state vector to separate the last 2 qubits 286 | reshaped_state = sv.reshape([2**2, 2**problem.N_qubits]) 287 | 288 | # Sum over the amplitudes of the last qubit to trace it out 289 | statevector[method] = np.sum(reshaped_state, axis=0) 290 | else: 291 | statevector[method] = Statevector(circuit).data 292 | 293 | self.assertTrue( 294 | np.allclose( 295 | statevector["PauliBasis"], statevector["Diffusion"], atol=1e-8 296 | ) 297 | ) 298 | self.assertTrue( 299 | np.allclose( 300 | statevector["PauliBasis"], statevector["PowerOfTwo"], atol=1e-8 301 | ) 302 | ) 303 | self.assertTrue( 304 | np.allclose( 305 | statevector["PowerOfTwo"], statevector["Diffusion"], atol=1e-8 306 | ) 307 | ) 308 | 309 | 310 | if __name__ == "__main__": 311 | unittest.main() 312 | --------------------------------------------------------------------------------