├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── ExactCover │ ├── ExactCover 6 3.ipynb │ ├── data │ │ └── FRCR_6_24_3.txt │ └── tailassignment_loader.py ├── MaxCut │ ├── CVaR.ipynb │ ├── ComparisonOptimizers.ipynb │ ├── ComparisonPostProcessing.ipynb │ ├── ConstrainedCase.ipynb │ ├── ToyExample.ipynb │ ├── WithFlip.ipynb │ └── data │ │ ├── w_ba_n10_k4_0.gml │ │ └── w_ba_n21_k4_0.gml ├── PortfolioOptimization │ ├── PortOpt.ipynb │ ├── asset_loader.py │ └── data │ │ └── qiskit_finance_seeds.npz └── plotroutines.py ├── images └── E.png ├── qaoa ├── __init__.py ├── initialstates │ ├── __init__.py │ ├── base_initialstate.py │ ├── dicke1_2_initialstate.py │ ├── dicke_initialstate.py │ ├── lessthank_initialstate.py │ ├── maxkcut_feasible_initialstate.py │ ├── plus_initialstate.py │ ├── statevector_initialstate.py │ └── tensor_initialstate.py ├── mixers │ ├── __init__.py │ ├── base_mixer.py │ ├── grover_mixer.py │ ├── maxkcut_grover_mixer.py │ ├── maxkcut_lx_mixer.py │ ├── x_mixer.py │ ├── xy_mixer.py │ └── xy_tensor.py ├── problems │ ├── __init__.py │ ├── base_problem.py │ ├── exactcover_problem.py │ ├── graph_problem.py │ ├── maxkcut_binary_fullH.py │ ├── maxkcut_binary_powertwo.py │ ├── maxkcut_one_hot_problem.py │ ├── portfolio_problem.py │ └── qubo_problem.py ├── qaoa.py └── util │ ├── __init__.py │ ├── flip.py │ ├── graphutils.py │ ├── post.py │ └── statistic.py ├── run_all.sh ├── run_graphs.py ├── run_graphs.sh ├── setup.py └── unittests ├── __init__.py ├── test_maxkcut_binary_problem.py ├── test_maxkcut_feasible_initialstate.py ├── test_maxkcut_mixers.py └── test_maxkcut_one_hot_problem.py /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | *** 130 | ### Tensorize mixers 131 | To tensorize a mixer, i.e. decomposing the mixer into a tensor product of unitaries that is 132 | performed on each qubit, one can call the tensor class with the arguments of mixer and number of qubits in subpart. 133 | 134 | For example, for the standard MaxCut problem above where the X mixer was used, one could find the tensor by writing: 135 | 136 | tensorized_mixer = Tensor(mixer.X(), number_of_qubits_of_subpart) 137 | 138 | 139 | *** 140 | ### Example usage 141 | 142 | See [examples here](examples/). 143 | 144 | 145 | *** 146 | ### Acknowledgement 147 | We would like to thank for funding of the work by the Research Council of Norway through project number 33202. 148 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/PortfolioOptimization/data/qiskit_finance_seeds.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/7497ae68533e6124981e6aff3364cab9114389b2/examples/PortfolioOptimization/data/qiskit_finance_seeds.npz -------------------------------------------------------------------------------- /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 | 5 | import numpy as np 6 | 7 | from qaoa import QAOA 8 | 9 | from qaoa.util import Statistic 10 | 11 | 12 | def __plot_landscape(A, extent, fig): 13 | if not fig: 14 | fig = plt.figure(figsize=(6, 6), dpi=80, facecolor="w", edgecolor="k") 15 | _ = plt.xlabel(r"$\gamma$") 16 | _ = plt.ylabel(r"$\beta$") 17 | ax = fig.gca() 18 | _ = plt.title("Expectation value") 19 | im = ax.imshow(A, interpolation="bicubic", origin="lower", extent=extent) 20 | divider = make_axes_locatable(ax) 21 | cax = divider.append_axes("right", size="5%", pad=0.05) 22 | _ = plt.colorbar(im, cax=cax) 23 | 24 | 25 | def plot_E(qaoa_instance, fig=None): 26 | angles = qaoa_instance.landscape_p1_angles 27 | extent = [ 28 | angles["gamma"][0], 29 | angles["gamma"][1], 30 | angles["beta"][0], 31 | angles["beta"][1], 32 | ] 33 | return __plot_landscape(qaoa_instance.exp_landscape(), extent, fig=fig) 34 | 35 | 36 | def plot_Var(qaoa_instance, fig=None): 37 | angles = qaoa_instance.landscape_p1_angles 38 | extent = [ 39 | angles["gamma"][0], 40 | angles["gamma"][1], 41 | angles["beta"][0], 42 | angles["beta"][1], 43 | ] 44 | return __plot_landscape(qaoa_instance.var_landscape(), extent, fig=fig) 45 | 46 | 47 | def plot_ApproximationRatio( 48 | qaoa_instance, maxdepth, mincost, maxcost, label, style="", fig=None, shots=None 49 | ): 50 | if not shots: 51 | exp = np.array(qaoa_instance.get_Exp()) 52 | else: 53 | exp = [] 54 | for p in range(1, qaoa_instance.current_depth + 1): 55 | ar, sp = __apprrat_successprob(qaoa_instance, p, shots=shots) 56 | exp.append(ar) 57 | exp = np.array(exp) 58 | 59 | if not fig: 60 | ax = plt.figure().gca() 61 | else: 62 | ax = fig.gca() 63 | plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black") 64 | plt.plot( 65 | np.arange(1, maxdepth + 1), 66 | (maxcost - exp) / (maxcost - mincost), 67 | style, 68 | label=label, 69 | ) 70 | plt.ylim(0, 1.01) 71 | plt.xlim(1 - 0.25, maxdepth + 0.25) 72 | _ = plt.ylabel("appr. ratio") 73 | _ = plt.xlabel("depth") 74 | _ = plt.legend(loc="lower right", framealpha=1) 75 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 76 | 77 | 78 | def plot_successprob(qaoa_instance, maxdepth, label, style="", fig=None, shots=10**4): 79 | successp = [] 80 | for p in range(1, qaoa_instance.current_depth + 1): 81 | ar, sp = __apprrat_successprob(qaoa_instance, p, shots=shots) 82 | successp.append(sp) 83 | successp = np.array(successp) 84 | 85 | if not fig: 86 | ax = plt.figure().gca() 87 | else: 88 | ax = fig.gca() 89 | plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black") 90 | plt.plot( 91 | np.arange(1, maxdepth + 1), 92 | successp, 93 | style, 94 | label=label, 95 | ) 96 | plt.ylim(0, 1.01) 97 | plt.xlim(1 - 0.25, maxdepth + 0.25) 98 | _ = plt.ylabel("success prob") 99 | _ = plt.xlabel("depth") 100 | _ = plt.legend(loc="lower right", framealpha=1) 101 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 102 | 103 | 104 | def __apprrat_successprob(qaoa_instance, depth, shots=10**4): 105 | """ 106 | approximation ratio post processed with feasibility and success probability 107 | """ 108 | hist = qaoa_instance.hist( 109 | qaoa_instance.optimization_results[depth].get_best_angles(), shots=shots 110 | ) 111 | 112 | counts = 0 113 | 114 | stat = Statistic(cvar=qaoa_instance.cvar) 115 | 116 | for string in hist: 117 | if qaoa_instance.problem.isFeasible(string): 118 | cost = qaoa_instance.problem.cost(string) 119 | counts += hist[string] 120 | stat.add_sample(cost, hist[string], string) 121 | 122 | return -stat.get_CVaR(), counts / shots 123 | 124 | 125 | def plot_angles(qaoa_instance, depth, label, style="", fig=None): 126 | angles = qaoa_instance.optimization_results[depth].get_best_angles() 127 | 128 | if not fig: 129 | ax = plt.figure().gca() 130 | else: 131 | ax = fig.gca() 132 | 133 | plt.plot( 134 | np.arange(1, depth + 1), 135 | angles[::2], 136 | "--" + style, 137 | label=r"$\gamma$ " + label, 138 | ) 139 | plt.plot( 140 | np.arange(1, depth + 1), 141 | angles[1::2], 142 | "-" + style, 143 | label=r"$\beta$ " + label, 144 | ) 145 | plt.xlim(1 - 0.25, depth + 0.25) 146 | _ = plt.ylabel("parameter") 147 | _ = plt.xlabel("depth") 148 | _ = plt.legend() 149 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 150 | 151 | 152 | def draw_colored_graph(G, edge_colors): 153 | # Draw the graph with colored edges 154 | # extend the color_map if necessary 155 | color_map = [ 156 | "red", 157 | "blue", 158 | "green", 159 | "purple", 160 | "orange", 161 | "pink", 162 | "brown", 163 | "gray", 164 | "yellow", 165 | "cyan", 166 | ] 167 | pos = nx.spring_layout(G) # Positions for all nodes 168 | 169 | # Draw nodes 170 | nx.draw_networkx_nodes(G, pos, node_size=700, node_color="lightgray") 171 | 172 | # Draw edges with colors 173 | for color_idx, edges in edge_colors.items(): 174 | nx.draw_networkx_edges( 175 | G, 176 | pos, 177 | edgelist=edges, 178 | width=2, 179 | edge_color=color_map[color_idx % len(color_map)], 180 | ) 181 | 182 | # Draw labels 183 | nx.draw_networkx_labels(G, pos, font_size=20, font_family="sans-serif") 184 | 185 | # Show the graph 186 | plt.axis("off") 187 | plt.show() 188 | -------------------------------------------------------------------------------- /images/E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/7497ae68533e6124981e6aff3364cab9114389b2/images/E.png -------------------------------------------------------------------------------- /qaoa/__init__.py: -------------------------------------------------------------------------------- 1 | from .qaoa import QAOA 2 | 3 | from . import mixers 4 | from . import problems 5 | from . import initialstates 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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(): Creates a circuit that is a 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 | Circuit to prepare a 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 | -------------------------------------------------------------------------------- /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, and it creates a circuit that creates 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/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, and it creates a quantum circuit that creates the initial state for the for the MAX k-CUT problem for 13 | different cases of k subsets. 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(): creates a circuit that can create the wanted initial state for the special k 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 k = a power of 2 73 | 74 | Returns: 75 | QuantumCircuit: circuit that creates the initial state for k = a power of 2 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: circuit that creates the initial state for k = 3 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: circuit that creates the initial state for k = 5 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: circuit that creates the initial state for k = 6 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: circuit that creates the initial state for k = 7 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 | -------------------------------------------------------------------------------- /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 | MaxKCutFeasible initial state. 14 | 15 | Subclass of the `InitialState` class, and it determines the feasible states for the type of MAX k-CUT problem that is specified by the arguments. This specifies the number of cuts, number of qubits per vertex, 16 | and method for solving the special case of k = 6. 17 | 18 | Attributes: 19 | k_cuts (int): The number of cuts (or "colors") of the vertices in the MAX k-CUT problem is separated into. 20 | problem_encoding (str): description of the type of problem, either "onehot" (which corresponds to ...) or "binary" (which corresponds to ...) 21 | color_encoding (str): determines the approach to solving the MAX k-cut problem by following one of three methods, 22 | either "Dicke1_2" (which corresponds to creating an initial state that is a superposition of the valid states that represents a color(only 6/8 possible states)), 23 | "LessThanK" (which corresponds to grouping states together and make the group represent one color), 24 | or "max_balanced" (which corresponds to the onehot case where a color corresponds to a state) 25 | 26 | Methods: 27 | create_circuit(): creates a circuit that creates an initial state for only feasible initial states of the MAX k-CUT problem given constraints 28 | """ 29 | def __init__( 30 | self, k_cuts: int, problem_encoding: str, color_encoding: str = "LessThanK" 31 | ) -> None: 32 | """ 33 | Args: 34 | k_cuts (int): 35 | problem_encoding (str): description of the type of problem, either "onehot" (which corresponds to ...) or "binary" (which corresponds to ...) 36 | color_encoding (str): determines the approach to solving the MAX k-cut problem by following one of three methods, 37 | either "Dicke1_2" (which corresponds to creating an initial state that is a superposition of the valid states that represents a color(only 6/8 possible states)), 38 | "LessThanK" (which corresponds to grouping states together and make the group represent one color), 39 | or "max_balanced" (which corresponds to the onehot case where a color corresponds to a state). Defaults to "LessThanK". 40 | 41 | """ 42 | self.k_cuts = k_cuts 43 | self.problem_encoding = problem_encoding 44 | self.color_encoding = color_encoding 45 | 46 | if not problem_encoding in ["onehot", "binary"]: 47 | raise ValueError('case must be in ["onehot", "binary"]') 48 | if problem_encoding == "binary": 49 | if k_cuts == 6 and (color_encoding not in ["Dicke1_2", "LessThanK"]): 50 | raise ValueError('color_encoding must be in ["LessThanK", "Dicke1_2"]') 51 | self.color_encoding = color_encoding 52 | 53 | if self.k_cuts == 3: 54 | self.infeasible = ["11"] 55 | elif self.k_cuts == 5: 56 | if self.color_encoding == "max_balanced": 57 | self.infeasible = ["100", "111", "101"] 58 | else: 59 | self.infeasible = ["101", "110", "111"] 60 | elif self.k_cuts == 6: 61 | if self.color_encoding in ["Dicke1_2", "max_balanced"]: 62 | self.infeasible = ["000", "111"] 63 | else: 64 | self.infeasible = ["110", "111"] 65 | elif self.k_cuts == 7: 66 | self.infeasible = ["111"] 67 | 68 | def create_circuit(self) -> None: 69 | """ 70 | Creates a circuit that creates the initial state (for only feasible states) for the MAX k-CUT problem given 71 | the methods and cuts given as arguments 72 | """ 73 | if self.problem_encoding == "binary": 74 | self.k_bits = int(np.ceil(np.log2(self.k_cuts))) 75 | self.num_V = self.N_qubits / self.k_bits 76 | 77 | if not self.num_V.is_integer(): 78 | raise ValueError( 79 | "Total qubits=" 80 | + str(self.N_qubits) 81 | + " is not a multiple of " 82 | + str(self.k_bits) 83 | ) 84 | if self.k_cuts == 6 and self.color_encoding == "Dicke1_2": 85 | circ_one_node = Dicke1_2() 86 | else: 87 | circ_one_node = LessThanK(self.k_cuts) 88 | 89 | elif self.problem_encoding == "onehot": 90 | self.num_V = self.N_qubits / self.k_cuts 91 | 92 | if not self.num_V.is_integer(): 93 | raise ValueError( 94 | "Total qubits=" 95 | + str(self.N_qubits) 96 | + " is not a multiple of " 97 | + str(self.k_cuts) 98 | ) 99 | self.num_V = int(self.num_V) 100 | 101 | circ_one_node = Dicke(1) 102 | circ_one_node.setNumQubits(self.k_cuts) 103 | 104 | self.num_V = int(self.num_V) 105 | self.tensor = Tensor(circ_one_node, self.num_V) 106 | 107 | self.tensor.create_circuit() 108 | self.circuit = self.tensor.circuit 109 | -------------------------------------------------------------------------------- /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, and it creates an initial plus state 12 | 13 | Methods: 14 | create_circuit(): Creates a circuit that sets up plus states for the initial states 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 an initial state that is a plus state 22 | """ 23 | q = QuantumRegister(self.N_qubits) 24 | self.circuit = QuantumCircuit(q) 25 | self.circuit.h(q) 26 | -------------------------------------------------------------------------------- /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. 10 | 11 | Subclass of the `InitialState` class, and it creates the initial statevector. 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 to be tensorized. 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 circuit that is to be tensorised 30 | """ 31 | self.subcircuit = subcircuit 32 | self.mixer_param = Parameter("x_beta") 33 | self.N_qubits = subcircuit.N_qubits 34 | 35 | def create_circuit(self): 36 | """ 37 | Constructs the Grover mixer circuit using the subcircuit. 38 | 39 | Given feasible states f \in F, 40 | and let US be the circuit that prepares US = 1/|F| \sum_{f\inF} |f>. 41 | The Grover mixer has the form US^\dagger X^n C^{n-1}Phase X^n US. 42 | """ 43 | 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/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/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 | -------------------------------------------------------------------------------- /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/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) -> None: 31 | """ 32 | Initializes the XY mixer. 33 | 34 | Args: 35 | topology (list, optional): The topology of the mixer. If None, defaults to "ring" topology. 36 | """ 37 | self.topology = topology 38 | self.mixer_param = Parameter("x_beta") 39 | 40 | def create_circuit(self): 41 | """ 42 | Constructs the XY mixer circuit using the specified topology. 43 | 44 | If no topology is specified, it defaults to a "ring" topology. 45 | """ 46 | if not self.topology: 47 | print('No topology specified for the XY-mixer, assuming "ring" topology') 48 | self.topology = XY.generate_pairs(self.N_qubits) 49 | 50 | q = QuantumRegister(self.N_qubits) 51 | self.circuit = QuantumCircuit(q) 52 | 53 | for i, e in enumerate(self.topology): 54 | self.circuit.append(XXPlusYYGate(0.5 * self.mixer_param), e) 55 | 56 | @staticmethod 57 | def generate_pairs(n, case="ring"): 58 | """_summary_ 59 | 60 | Args: 61 | n (int): The number of qubits. 62 | case (str, optional): Topology. Defaults to "ring". 63 | 64 | Returns: 65 | list: A list of pairs of qubit indices based on the specified topology. 66 | """ 67 | # default ring, otherwise "chain" 68 | if n < 2: 69 | return [] # Not enough elements to form any pairs 70 | 71 | pairs = [[i, i + 1] for i in range(n - 1)] 72 | 73 | if case == "ring": 74 | pairs.append([n - 1, 0]) 75 | 76 | return pairs 77 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, and it 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/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, and it 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/problems/maxkcut_binary_fullH.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 | from .maxkcut_binary_powertwo import MaxKCutBinaryPowerOfTwo 14 | 15 | 16 | class MaxKCutBinaryFullH(GraphProblem): 17 | """ 18 | Max k-CUT Binary Full H graph problem. 19 | 20 | Subclass of the `GraphProblem` class, and it implements the Max k-Cut problem using a binary encoding and full Hamiltonian construction for QAOA. 21 | This class supports several encoding and circuit construction methods for different values of k, and allows for 22 | flexible color encodings and optional node fixing. 23 | 24 | Attributes: 25 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 26 | k_cuts (int): The number of partitions (colors) to cut the graph into. 27 | color_encoding (str): The encoding scheme for colors, e.g., "LessThanK" or "max_balanced". 28 | method (str): The method used for circuit construction, one of "PauliBasis", "PowerOfTwo", or "Diffusion". 29 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 30 | N_qubits_per_node (int): Number of qubits used to encode each node. 31 | colors (dict): Maps color labels to lists of binary strings representing that color. 32 | bitstring_to_color (dict): Maps each binary string to its corresponding color label. 33 | mkcb_pot (MaxKCutBinaryPowerOfTwo): Helper instance for the "PowerOfTwo" method (if used). 34 | N_ancilla_qubits (int): Number of ancilla qubits required for the "PowerOfTwo" method. 35 | op (SparsePauliOp): The full Pauli operator for the cost Hamiltonian (if using "PauliBasis"). 36 | ophalf (SparsePauliOp): The half Pauli operator for the fixed-node case (if using "PauliBasis"). 37 | 38 | Methods: 39 | validate_parameters(k, method, fix_one_node): Validates the input parameters for k, method, and fix_one_node. 40 | construct_colors(): Constructs the mapping from binary strings to color classes based on k and encoding. 41 | apply_N(circuit, binary_str1, binary_str2): Applies X gates to the circuit to map between two binary color encodings. 42 | add_equalize_color(qc, bs1, bs2, theta): Adds gates to the circuit to equalize the phase between two color encodings. 43 | create_edge_circuit(theta): Creates the parameterized quantum circuit for an edge, according to the chosen method. 44 | create_edge_circuit_fixed_node(theta): Creates the parameterized quantum circuit for an edge when one node is fixed. 45 | getPauliOperator(k_cuts, color_encoding): Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 46 | """ 47 | def __init__( 48 | self, 49 | G: nx.Graph, 50 | k_cuts: int, 51 | color_encoding: str, 52 | method: str = "Diffusion", 53 | fix_one_node: bool = False, # this fixes the last node to color 1, i.e., one qubit gets removed 54 | ) -> None: 55 | """ 56 | Args: 57 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 58 | k_cuts (int): The number of partitions (colors) to cut the graph into. 59 | color_encoding (str): The encoding scheme for colors, e.g., "LessThanK" or "max_balanced". 60 | method (str): The method used for circuit construction, one of "PauliBasis", "PowerOfTwo", or "Diffusion". 61 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 62 | """ 63 | MaxKCutBinaryFullH.validate_parameters(k_cuts, method, fix_one_node) 64 | 65 | self.k_cuts = k_cuts 66 | self.color_encoding = color_encoding 67 | self.method = method 68 | 69 | N_qubits_per_node = int(np.ceil(np.log2(self.k_cuts))) 70 | super().__init__(G, N_qubits_per_node, fix_one_node) 71 | 72 | if self.method == "PauliBasis": 73 | self.op, self.ophalf = self.getPauliOperator( 74 | self.k_cuts, color_encoding=self.color_encoding 75 | ) 76 | elif self.method == "PowerOfTwo": 77 | if self.k_cuts == 3: 78 | k = 4 79 | else: 80 | k = 8 81 | self.mkcb_pot = MaxKCutBinaryPowerOfTwo(G, k, method="Diffusion") 82 | self.N_ancilla_qubits = 2 83 | else: # if self.method == "Diffusion": 84 | pass 85 | 86 | self.construct_colors() 87 | 88 | @staticmethod 89 | def validate_parameters(k, method, fix_one_node) -> None: 90 | """ 91 | Validates the input parameters for k, method, and fix_one_node. 92 | 93 | Args: 94 | k (int): Number of partitions (colors). 95 | method (str): Circuit construction method ("PauliBasis", "PowerOfTwo", or "Diffusion"). 96 | fix_one_node (bool): Whether to fix the last node to a specific color. 97 | 98 | Raises: 99 | ValueError: If k is not in [3, 5, 6, 7]. 100 | ValueError: If method is not valid. 101 | ValueError: If method is "PowerOfTwo" and fix_one_node is True. 102 | """ 103 | ### 1) k_cuts needs to be 3, 5, 6, or 7 104 | valid_ks = [3, 5, 6, 7] 105 | if k not in valid_ks: 106 | raise ValueError("k_cuts must be in " + str(valid_ks)) 107 | 108 | ### 2) method 109 | valid_methods = ["PauliBasis", "PowerOfTwo", "Diffusion"] 110 | if method not in valid_methods: 111 | raise ValueError("method must be in " + str(valid_methods)) 112 | 113 | if method == "PowerOfTwo" and fix_one_node: 114 | raise ValueError( 115 | 'For the PowerOfTwo method it "fix_one_node" is not implemented. Use "PauliBasis" or "Diffusion" instead.' 116 | ) 117 | 118 | def construct_colors(self): 119 | """ 120 | Constructs the mapping from binary strings to color classes based on k and encoding. 121 | 122 | Raises: 123 | ValueError: If color_encoding is invalid or unspecified for the given k. 124 | """ 125 | if self.k_cuts == 3: 126 | if self.color_encoding == "LessThanK": 127 | self.colors = { 128 | "color1": ["00"], 129 | "color2": ["01"], 130 | "color3": ["10", "11"], 131 | } 132 | else: 133 | raise ValueError("invalid or unspecified color_encoding") 134 | elif self.k_cuts == 5: 135 | if self.color_encoding == "LessThanK": 136 | self.colors = { 137 | "color1": ["000"], 138 | "color2": ["001"], 139 | "color3": ["010"], 140 | "color4": ["011"], 141 | "color5": ["100", "101", "110", "111"], 142 | } 143 | elif self.color_encoding == "max_balanced": 144 | self.colors = { 145 | "color1": ["000", "001"], 146 | "color2": ["010"], 147 | "color3": ["011"], 148 | "color4": ["100", "101"], 149 | "color5": ["110", "111"], 150 | } 151 | else: 152 | raise ValueError("invalid or unspecified color_encoding") 153 | elif self.k_cuts == 6: 154 | if self.color_encoding == "LessThanK": 155 | self.colors = { 156 | "color1": ["000"], 157 | "color2": ["001"], 158 | "color3": ["010"], 159 | "color4": ["011"], 160 | "color5": ["100"], 161 | "color6": ["101", "110", "111"], 162 | } 163 | elif self.color_encoding in ["max_balanced"]: 164 | self.colors = { 165 | "color1": ["000", "001"], 166 | "color2": ["010"], 167 | "color3": ["011"], 168 | "color4": ["100", "101"], 169 | "color5": ["110"], 170 | "color6": ["111"], 171 | } 172 | else: 173 | raise ValueError("invalid or unspecified color_encoding") 174 | else: # if self.k_cuts == 7: 175 | if self.color_encoding == "LessThanK": 176 | self.colors = { 177 | "color1": ["000"], 178 | "color2": ["001"], 179 | "color3": ["010"], 180 | "color4": ["011"], 181 | "color5": ["100"], 182 | "color6": ["101"], 183 | "color7": ["110", "111"], 184 | } 185 | else: 186 | raise ValueError("invalid or unspecified color_encoding") 187 | # Create a dictionary to map each index to its corresponding set 188 | self.bitstring_to_color = {} 189 | for key, indices in self.colors.items(): 190 | for index in indices: 191 | self.bitstring_to_color[index] = key 192 | 193 | def apply_N(self, circuit, binary_str1, binary_str2): 194 | """ 195 | Applies X gates to the circuit to map between two binary color encodings. 196 | 197 | Args: 198 | circuit (QuantumCircuit): The quantum circuit to modify. 199 | binary_str1 (str): Binary string for the first color encoding. 200 | binary_str2 (str): Binary string for the second color encoding. 201 | 202 | Returns: 203 | circuit (QuantumCircuit): The modified quantum circuit. 204 | """ 205 | # Apply X-gates based on the first binary string 206 | for i, bit in enumerate(binary_str1): 207 | if bit == "0": 208 | circuit.x(i) 209 | 210 | # Apply X-gates based on the second binary string 211 | for i, bit in enumerate(binary_str2): 212 | if bit == "0": 213 | circuit.x(self.N_qubits_per_node + i) 214 | 215 | return circuit 216 | 217 | def add_equalize_color(self, qc, bs1, bs2, theta): 218 | """ 219 | Adds gates to the circuit to equalize the phase between two color encodings. 220 | 221 | Args: 222 | qc (QuantumCircuit): The quantum circuit to modify. 223 | bs1 (str): Binary string for the first color encoding. 224 | bs2 (str): Binary string for the second color encoding. 225 | theta (float): The phase parameter. 226 | 227 | Returns: 228 | qc (QuantumCircuit): The modified quantum circuit. 229 | """ 230 | qc = self.apply_N(qc, bs1, bs2) 231 | qc.barrier() 232 | qc.mcx( 233 | [qc.qubits[i] for i in range(0, self.N_qubits_per_node)], 234 | [qc.qubits[2 * self.N_qubits_per_node]], 235 | ) 236 | qc.mcx( 237 | [ 238 | qc.qubits[self.N_qubits_per_node + i] 239 | for i in range(0, self.N_qubits_per_node) 240 | ], 241 | [qc.qubits[2 * self.N_qubits_per_node + 1]], 242 | ) 243 | # C^{n-1}Phase 244 | phase_gate = PhaseGate(-theta).control(2) 245 | qc.append( 246 | phase_gate, 247 | [ 248 | 2 * self.N_qubits_per_node + 1, 249 | 2 * self.N_qubits_per_node, 250 | 2 * self.N_qubits_per_node - 1, 251 | ], 252 | ) 253 | qc.mcx( 254 | [ 255 | qc.qubits[self.N_qubits_per_node + i] 256 | for i in range(0, self.N_qubits_per_node) 257 | ], 258 | [qc.qubits[2 * self.N_qubits_per_node + 1]], 259 | ) 260 | qc.mcx( 261 | [qc.qubits[i] for i in range(0, self.N_qubits_per_node)], 262 | [qc.qubits[2 * self.N_qubits_per_node]], 263 | ) 264 | qc.barrier() 265 | qc = self.apply_N(qc, bs1, bs2) 266 | 267 | return qc 268 | 269 | def create_edge_circuit(self, theta): 270 | """ 271 | Creates the parameterized quantum circuit for a graph edge according to the chosen method and color encoding. 272 | 273 | Args: 274 | theta (float): The phase parameter for the circuit. 275 | 276 | Returns: 277 | qc (QuantumCircuit): The constructed quantum circuit for the edge. 278 | """ 279 | q = QuantumRegister(2 * self.N_qubits_per_node) 280 | if self.method == "PauliBasis": 281 | qc = QuantumCircuit(q) 282 | qc.append(PauliEvolutionGate(self.op, time=theta), qc.qubits) 283 | elif self.method == "PowerOfTwo": 284 | a = AncillaRegister(self.N_ancilla_qubits) 285 | qc = QuantumCircuit(q, a) 286 | 287 | qubits_to_map = list(range(2 * self.N_qubits_per_node)) 288 | ancilla_to_map = [] 289 | qc.append( 290 | self.mkcb_pot.create_edge_circuit(theta), 291 | q[qubits_to_map] + (a[ancilla_to_map] if ancilla_to_map else []), 292 | ) 293 | 294 | for _, bitstrings in self.colors.items(): 295 | if len(bitstrings) > 1: 296 | pairs = list(itertools.combinations(bitstrings, 2)) 297 | for bs1, bs2 in pairs: 298 | qc = self.add_equalize_color(qc, bs1, bs2, theta) 299 | qc = self.add_equalize_color(qc, bs2, bs1, theta) 300 | 301 | # target_qubits = [q[i] for i in range(2*self.N_qubits_per_node)] + [ 302 | # a[i] for i in range(2) 303 | # ] # Map k qubits and 2 ancillas 304 | # parameterized_circuit.append(small_circuit.to_instruction(), target_qubits) 305 | 306 | else: # if self.method == "Diffusion": 307 | qc = QuantumCircuit(q) 308 | if self.k_cuts == 3: 309 | phase_gate = PhaseGate(-theta).control(1) 310 | qc.append(phase_gate, [0, 2]) 311 | qc.cx(1, 3) 312 | qc.x([0, 2, 3]) 313 | phase_gate = PhaseGate(-theta).control(2) 314 | qc.append(phase_gate, [0, 2, 3]) 315 | qc.x([0, 2, 3]) 316 | qc.cx(1, 3) 317 | elif self.k_cuts == 5: 318 | if self.color_encoding == "max_balanced": 319 | qc.cx(0, 3) 320 | qc.cx(1, 4) 321 | qc.cx(2, 5) 322 | qc.x([3, 4, 5]) 323 | phase_gate = PhaseGate(-theta).control(2) 324 | qc.append(phase_gate, [3, 4, 5]) 325 | qc.x([3, 4, 5]) 326 | qc.cx(2, 5) 327 | qc.cx(1, 4) 328 | qc.cx(5, 2, ctrl_state=0) 329 | qc.x([1, 2, 3, 4]) 330 | phase_gate = PhaseGate(-theta).control(3) 331 | qc.append(phase_gate, [1, 2, 3, 4]) 332 | qc.x([1, 2, 3, 4]) 333 | qc.cx(5, 2, ctrl_state=0) 334 | qc.cx(0, 3) 335 | qc.cx(2, 5) 336 | phase_gate = PhaseGate(-theta).control(4) 337 | qc.append(phase_gate, [0, 1, 3, 4, 5]) 338 | qc.cx(2, 5) 339 | else: # self.color_encoding=="LessThanK": 340 | phase_gate = PhaseGate(-theta).control(1) 341 | qc.append(phase_gate, [0, 3]) 342 | qc.cx(1, 4) 343 | qc.cx(2, 5) 344 | qc.x([0, 3, 4, 5]) 345 | phase_gate = PhaseGate(-theta).control(3) 346 | qc.append(phase_gate, [0, 3, 4, 5]) 347 | qc.x([0, 3, 4, 5]) 348 | qc.cx(2, 5) 349 | qc.cx(1, 4) 350 | elif self.k_cuts == 6: 351 | if self.color_encoding == "max_balanced": 352 | qc.cx(0, 3) 353 | qc.cx(1, 4) 354 | qc.cx(2, 5) 355 | qc.x([3, 4, 5]) 356 | phase_gate = PhaseGate(-theta).control(2) 357 | qc.append(phase_gate, [3, 4, 5]) 358 | qc.x([3, 4, 5]) 359 | qc.cx(2, 5) 360 | qc.cx(1, 4) 361 | qc.cx(5, 2, ctrl_state=0) 362 | qc.x([1, 2, 3, 4]) 363 | phase_gate = PhaseGate(-theta).control(3) 364 | qc.append(phase_gate, [1, 2, 3, 4]) 365 | qc.x([1, 2, 3, 4]) 366 | qc.cx(5, 2, ctrl_state=0) 367 | qc.cx(0, 3) 368 | else: # self.color_encoding=="LessThanK": 369 | qc.cx(0, 3) 370 | qc.cx(1, 4) 371 | qc.cx(2, 5) 372 | qc.x([3, 4, 5]) 373 | phase_gate = PhaseGate(-theta).control(2) 374 | qc.append(phase_gate, [3, 4, 5]) 375 | qc.x([3, 4, 5]) 376 | qc.cx(2, 5) 377 | qc.cx(1, 4) 378 | qc.cx(0, 3) 379 | qc.ccx(2, 4, 5) 380 | phase_gate = PhaseGate(-theta).control(3) 381 | qc.append(phase_gate, [0, 1, 3, 5]) 382 | qc.ccx(2, 4, 5) 383 | qc.x(1) 384 | phase_gate = PhaseGate(-theta).control(4) 385 | qc.append(phase_gate, [0, 1, 2, 3, 4]) 386 | qc.x(1) 387 | elif self.k_cuts == 7: 388 | qc.cx(0, 3) 389 | qc.cx(1, 4) 390 | qc.cx(2, 5) 391 | qc.x([3, 4, 5]) 392 | phase_gate = PhaseGate(-theta).control(2) 393 | qc.append(phase_gate, [3, 4, 5]) 394 | qc.x(5) 395 | phase_gate = PhaseGate(-theta).control(4) 396 | qc.append(phase_gate, [0, 1, 3, 4, 5]) 397 | qc.x([3, 4]) 398 | qc.cx(2, 5) 399 | qc.cx(1, 4) 400 | qc.cx(0, 3) 401 | 402 | return qc 403 | 404 | def create_edge_circuit_fixed_node(self, theta): 405 | """ 406 | Creates the parameterized quantum circuit for an edge when one node is fixed to a specific color. 407 | 408 | Args: 409 | theta (float): The phase parameter for the circuit. 410 | 411 | Returns: 412 | qc (QuantumCircuit): The constructed quantum circuit for the edge with a fixed node. 413 | """ 414 | if self.method == "PauliBasis": 415 | qc = QuantumCircuit(self.N_qubits_per_node) 416 | qc.append(PauliEvolutionGate(self.ophalf, time=theta), qc.qubits) 417 | else: 418 | qc = self.mkcb_pot.create_edge_circuit_fixed_node(theta) 419 | return qc 420 | 421 | def getPauliOperator(self, k_cuts, color_encoding): 422 | """ 423 | Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 424 | 425 | Args: 426 | k_cuts (int): Number of partitions (colors). 427 | color_encoding (str): The encoding scheme for colors. 428 | 429 | Raises: 430 | ValueError: If color_encoding is invalid or unspecified for the given k. 431 | 432 | Returns: 433 | op (SparsePauliOp): The full Pauli operator for the cost Hamiltonian. 434 | ophalf (SparsePauliOp): The half Pauli operator for the fixed-node case. 435 | """ 436 | # flip Pauli strings, because of qiskit's little endian encoding 437 | if k_cuts == 3: 438 | if color_encoding == "LessThanK": 439 | P = [ 440 | [-4 / (2**4), Pauli("IIII"[::-1])], 441 | [-4 / (2**4), Pauli("IIZI"[::-1])], 442 | [+4 / (2**4), Pauli("IZIZ"[::-1])], 443 | [+4 / (2**4), Pauli("IZZZ"[::-1])], 444 | [-4 / (2**4), Pauli("ZIII"[::-1])], 445 | [+12 / (2**4), Pauli("ZIZI"[::-1])], 446 | [+4 / (2**4), Pauli("ZZIZ"[::-1])], 447 | [+4 / (2**4), Pauli("ZZZZ"[::-1])], 448 | ] 449 | Phalf = [ 450 | [-8 / (2**4), Pauli("II"[::-1])], 451 | [+8 / (2**4), Pauli("ZI"[::-1])], 452 | [+8 / (2**4), Pauli("IZ"[::-1])], 453 | [+8 / (2**4), Pauli("ZZ"[::-1])], 454 | ] 455 | else: 456 | raise ValueError("invalid or unspecified color_encoding") 457 | elif k_cuts == 5: 458 | if color_encoding == "max_balanced": 459 | # ((0, 1), (2,), (3,), (4,5), (6,7,)): 460 | P = [ 461 | [-36 / (2**6), Pauli("IIIIII"[::-1])], 462 | [4 / (2**6), Pauli("IIIIZI"[::-1])], 463 | [-4 / (2**6), Pauli("IIIZII"[::-1])], 464 | [4 / (2**6), Pauli("IIIZZI"[::-1])], 465 | [4 / (2**6), Pauli("IIZIIZ"[::-1])], 466 | [-4 / (2**6), Pauli("IIZIZZ"[::-1])], 467 | [4 / (2**6), Pauli("IIZZIZ"[::-1])], 468 | [-4 / (2**6), Pauli("IIZZZZ"[::-1])], 469 | [4 / (2**6), Pauli("IZIIII"[::-1])], 470 | [28 / (2**6), Pauli("IZIIZI"[::-1])], 471 | [4 / (2**6), Pauli("IZIZII"[::-1])], 472 | [-4 / (2**6), Pauli("IZIZZI"[::-1])], 473 | [-4 / (2**6), Pauli("IZZIIZ"[::-1])], 474 | [4 / (2**6), Pauli("IZZIZZ"[::-1])], 475 | [-4 / (2**6), Pauli("IZZZIZ"[::-1])], 476 | [4 / (2**6), Pauli("IZZZZZ"[::-1])], 477 | [-4 / (2**6), Pauli("ZIIIII"[::-1])], 478 | [4 / (2**6), Pauli("ZIIIZI"[::-1])], 479 | [28 / (2**6), Pauli("ZIIZII"[::-1])], 480 | [4 / (2**6), Pauli("ZIIZZI"[::-1])], 481 | [4 / (2**6), Pauli("ZIZIIZ"[::-1])], 482 | [-4 / (2**6), Pauli("ZIZIZZ"[::-1])], 483 | [4 / (2**6), Pauli("ZIZZIZ"[::-1])], 484 | [-4 / (2**6), Pauli("ZIZZZZ"[::-1])], 485 | [4 / (2**6), Pauli("ZZIIII"[::-1])], 486 | [-4 / (2**6), Pauli("ZZIIZI"[::-1])], 487 | [4 / (2**6), Pauli("ZZIZII"[::-1])], 488 | [28 / (2**6), Pauli("ZZIZZI"[::-1])], 489 | [-4 / (2**6), Pauli("ZZZIIZ"[::-1])], 490 | [4 / (2**6), Pauli("ZZZIZZ"[::-1])], 491 | [-4 / (2**6), Pauli("ZZZZIZ"[::-1])], 492 | [4 / (2**6), Pauli("ZZZZZZ"[::-1])], 493 | ] 494 | Phalf = [ 495 | [-32 / (2**6), Pauli("III"[::-1])], 496 | [32 / (2**6), Pauli("IZI"[::-1])], 497 | [32 / (2**6), Pauli("ZII"[::-1])], 498 | [32 / (2**6), Pauli("ZZI"[::-1])], 499 | ] 500 | elif color_encoding == "LessThanK": 501 | P = [ 502 | [-24 / (2**6), Pauli("IIIIII"[::-1])], 503 | [-24 / (2**6), Pauli("IIIZII"[::-1])], 504 | [+8 / (2**6), Pauli("IIZIIZ"[::-1])], 505 | [+8 / (2**6), Pauli("IIZZIZ"[::-1])], 506 | [+8 / (2**6), Pauli("IZIIZI"[::-1])], 507 | [+8 / (2**6), Pauli("IZIZZI"[::-1])], 508 | [+8 / (2**6), Pauli("IZZIZZ"[::-1])], 509 | [+8 / (2**6), Pauli("IZZZZZ"[::-1])], 510 | [-24 / (2**6), Pauli("ZIIIII"[::-1])], 511 | [+40 / (2**6), Pauli("ZIIZII"[::-1])], 512 | [+8 / (2**6), Pauli("ZIZIIZ"[::-1])], 513 | [+8 / (2**6), Pauli("ZIZZIZ"[::-1])], 514 | [+8 / (2**6), Pauli("ZZIIZI"[::-1])], 515 | [+8 / (2**6), Pauli("ZZIZZI"[::-1])], 516 | [+8 / (2**6), Pauli("ZZZIZZ"[::-1])], 517 | [+8 / (2**6), Pauli("ZZZZZZ"[::-1])], 518 | ] 519 | Phalf = [ 520 | [-48 / (2**6), Pauli("III"[::-1])], 521 | [+16 / (2**6), Pauli("ZII"[::-1])], 522 | [+16 / (2**6), Pauli("IIZ"[::-1])], 523 | [+16 / (2**6), Pauli("ZIZ"[::-1])], 524 | [+16 / (2**6), Pauli("IZI"[::-1])], 525 | [+16 / (2**6), Pauli("ZZI"[::-1])], 526 | [+16 / (2**6), Pauli("IZZ"[::-1])], 527 | [+16 / (2**6), Pauli("ZZZ"[::-1])], 528 | ] 529 | else: 530 | raise ValueError("invalid or unspecified color_encoding") 531 | elif k_cuts == 6: 532 | if color_encoding in ["max_balanced"]: 533 | # ((0,1), (2), (3), (4,5), (6), (7)) 534 | P = [ 535 | [-40 / (2**6), Pauli("IIIIII"[::-1])], 536 | [8 / (2**6), Pauli("IIIIZI"[::-1])], 537 | [8 / (2**6), Pauli("IIZIIZ"[::-1])], 538 | [-8 / (2**6), Pauli("IIZIZZ"[::-1])], 539 | [8 / (2**6), Pauli("IZIIII"[::-1])], 540 | [24 / (2**6), Pauli("IZIIZI"[::-1])], 541 | [-8 / (2**6), Pauli("IZZIIZ"[::-1])], 542 | [8 / (2**6), Pauli("IZZIZZ"[::-1])], 543 | [24 / (2**6), Pauli("ZIIZII"[::-1])], 544 | [8 / (2**6), Pauli("ZIIZZI"[::-1])], 545 | [8 / (2**6), Pauli("ZIZZIZ"[::-1])], 546 | [-8 / (2**6), Pauli("ZIZZZZ"[::-1])], 547 | [8 / (2**6), Pauli("ZZIZII"[::-1])], 548 | [24 / (2**6), Pauli("ZZIZZI"[::-1])], 549 | [-8 / (2**6), Pauli("ZZZZIZ"[::-1])], 550 | [8 / (2**6), Pauli("ZZZZZZ"[::-1])], 551 | ] 552 | Phalf = [ 553 | [-32 / (2**6), Pauli("III"[::-1])], 554 | [32 / (2**6), Pauli("IZI"[::-1])], 555 | [32 / (2**6), Pauli("ZII"[::-1])], 556 | [32 / (2**6), Pauli("ZZI"[::-1])], 557 | ] 558 | elif color_encoding == "LessThanK": 559 | P = [ 560 | [-36 / (2**6), Pauli("IIIIII"[::-1])], 561 | [-4 / (2**6), Pauli("IIIIIZ"[::-1])], 562 | [-4 / (2**6), Pauli("IIIIZI"[::-1])], 563 | [-4 / (2**6), Pauli("IIIIZZ"[::-1])], 564 | [-12 / (2**6), Pauli("IIIZII"[::-1])], 565 | [+4 / (2**6), Pauli("IIIZIZ"[::-1])], 566 | [+4 / (2**6), Pauli("IIIZZI"[::-1])], 567 | [+4 / (2**6), Pauli("IIIZZZ"[::-1])], 568 | [-4 / (2**6), Pauli("IIZIII"[::-1])], 569 | [+12 / (2**6), Pauli("IIZIIZ"[::-1])], 570 | [+4 / (2**6), Pauli("IIZIZI"[::-1])], 571 | [+4 / (2**6), Pauli("IIZIZZ"[::-1])], 572 | [+4 / (2**6), Pauli("IIZZII"[::-1])], 573 | [+4 / (2**6), Pauli("IIZZIZ"[::-1])], 574 | [-4 / (2**6), Pauli("IIZZZI"[::-1])], 575 | [-4 / (2**6), Pauli("IIZZZZ"[::-1])], 576 | [-4 / (2**6), Pauli("IZIIII"[::-1])], 577 | [+4 / (2**6), Pauli("IZIIIZ"[::-1])], 578 | [+12 / (2**6), Pauli("IZIIZI"[::-1])], 579 | [+4 / (2**6), Pauli("IZIIZZ"[::-1])], 580 | [+4 / (2**6), Pauli("IZIZII"[::-1])], 581 | [-4 / (2**6), Pauli("IZIZIZ"[::-1])], 582 | [+4 / (2**6), Pauli("IZIZZI"[::-1])], 583 | [-4 / (2**6), Pauli("IZIZZZ"[::-1])], 584 | [-4 / (2**6), Pauli("IZZIII"[::-1])], 585 | [+4 / (2**6), Pauli("IZZIIZ"[::-1])], 586 | [+4 / (2**6), Pauli("IZZIZI"[::-1])], 587 | [+12 / (2**6), Pauli("IZZIZZ"[::-1])], 588 | [+4 / (2**6), Pauli("IZZZII"[::-1])], 589 | [-4 / (2**6), Pauli("IZZZIZ"[::-1])], 590 | [-4 / (2**6), Pauli("IZZZZI"[::-1])], 591 | [+4 / (2**6), Pauli("IZZZZZ"[::-1])], 592 | [-12 / (2**6), Pauli("ZIIIII"[::-1])], 593 | [+4 / (2**6), Pauli("ZIIIIZ"[::-1])], 594 | [+4 / (2**6), Pauli("ZIIIZI"[::-1])], 595 | [+4 / (2**6), Pauli("ZIIIZZ"[::-1])], 596 | [+28 / (2**6), Pauli("ZIIZII"[::-1])], 597 | [-4 / (2**6), Pauli("ZIIZIZ"[::-1])], 598 | [-4 / (2**6), Pauli("ZIIZZI"[::-1])], 599 | [-4 / (2**6), Pauli("ZIIZZZ"[::-1])], 600 | [+4 / (2**6), Pauli("ZIZIII"[::-1])], 601 | [+4 / (2**6), Pauli("ZIZIIZ"[::-1])], 602 | [-4 / (2**6), Pauli("ZIZIZI"[::-1])], 603 | [-4 / (2**6), Pauli("ZIZIZZ"[::-1])], 604 | [-4 / (2**6), Pauli("ZIZZII"[::-1])], 605 | [+12 / (2**6), Pauli("ZIZZIZ"[::-1])], 606 | [+4 / (2**6), Pauli("ZIZZZI"[::-1])], 607 | [+4 / (2**6), Pauli("ZIZZZZ"[::-1])], 608 | [+4 / (2**6), Pauli("ZZIIII"[::-1])], 609 | [-4 / (2**6), Pauli("ZZIIIZ"[::-1])], 610 | [+4 / (2**6), Pauli("ZZIIZI"[::-1])], 611 | [-4 / (2**6), Pauli("ZZIIZZ"[::-1])], 612 | [-4 / (2**6), Pauli("ZZIZII"[::-1])], 613 | [+4 / (2**6), Pauli("ZZIZIZ"[::-1])], 614 | [+12 / (2**6), Pauli("ZZIZZI"[::-1])], 615 | [+4 / (2**6), Pauli("ZZIZZZ"[::-1])], 616 | [+4 / (2**6), Pauli("ZZZIII"[::-1])], 617 | [-4 / (2**6), Pauli("ZZZIIZ"[::-1])], 618 | [-4 / (2**6), Pauli("ZZZIZI"[::-1])], 619 | [+4 / (2**6), Pauli("ZZZIZZ"[::-1])], 620 | [-4 / (2**6), Pauli("ZZZZII"[::-1])], 621 | [+4 / (2**6), Pauli("ZZZZIZ"[::-1])], 622 | [+4 / (2**6), Pauli("ZZZZZI"[::-1])], 623 | [+12 / (2**6), Pauli("ZZZZZZ"[::-1])], 624 | ] 625 | Phalf = [ 626 | [-48 / (2**6), Pauli("III"[::-1])], 627 | [+16 / (2**6), Pauli("IIZ"[::-1])], 628 | [+16 / (2**6), Pauli("IZI"[::-1])], 629 | [+16 / (2**6), Pauli("IZZ"[::-1])], 630 | [+16 / (2**6), Pauli("ZII"[::-1])], 631 | [+16 / (2**6), Pauli("ZIZ"[::-1])], 632 | [+16 / (2**6), Pauli("ZZI"[::-1])], 633 | [+16 / (2**6), Pauli("ZZZ"[::-1])], 634 | ] 635 | else: 636 | raise ValueError("invalid or unspecified color_encoding") 637 | elif k_cuts == 7: 638 | if color_encoding == "LessThanK": 639 | P = [ 640 | [-44 / (2**6), Pauli("IIIIII"[::-1])], 641 | [-4 / (2**6), Pauli("IIIIZI"[::-1])], 642 | [-4 / (2**6), Pauli("IIIZII"[::-1])], 643 | [+4 / (2**6), Pauli("IIIZZI"[::-1])], 644 | [+12 / (2**6), Pauli("IIZIIZ"[::-1])], 645 | [+4 / (2**6), Pauli("IIZIZZ"[::-1])], 646 | [+4 / (2**6), Pauli("IIZZIZ"[::-1])], 647 | [-4 / (2**6), Pauli("IIZZZZ"[::-1])], 648 | [-4 / (2**6), Pauli("IZIIII"[::-1])], 649 | [+20 / (2**6), Pauli("IZIIZI"[::-1])], 650 | [+4 / (2**6), Pauli("IZIZII"[::-1])], 651 | [-4 / (2**6), Pauli("IZIZZI"[::-1])], 652 | [+4 / (2**6), Pauli("IZZIIZ"[::-1])], 653 | [+12 / (2**6), Pauli("IZZIZZ"[::-1])], 654 | [-4 / (2**6), Pauli("IZZZIZ"[::-1])], 655 | [+4 / (2**6), Pauli("IZZZZZ"[::-1])], 656 | [-4 / (2**6), Pauli("ZIIIII"[::-1])], 657 | [+4 / (2**6), Pauli("ZIIIZI"[::-1])], 658 | [+20 / (2**6), Pauli("ZIIZII"[::-1])], 659 | [-4 / (2**6), Pauli("ZIIZZI"[::-1])], 660 | [+4 / (2**6), Pauli("ZIZIIZ"[::-1])], 661 | [-4 / (2**6), Pauli("ZIZIZZ"[::-1])], 662 | [+12 / (2**6), Pauli("ZIZZIZ"[::-1])], 663 | [+4 / (2**6), Pauli("ZIZZZZ"[::-1])], 664 | [+4 / (2**6), Pauli("ZZIIII"[::-1])], 665 | [-4 / (2**6), Pauli("ZZIIZI"[::-1])], 666 | [-4 / (2**6), Pauli("ZZIZII"[::-1])], 667 | [+20 / (2**6), Pauli("ZZIZZI"[::-1])], 668 | [-4 / (2**6), Pauli("ZZZIIZ"[::-1])], 669 | [+4 / (2**6), Pauli("ZZZIZZ"[::-1])], 670 | [+4 / (2**6), Pauli("ZZZZIZ"[::-1])], 671 | [+12 / (2**6), Pauli("ZZZZZZ"[::-1])], 672 | ] 673 | Phalf = [ 674 | [-48 / (2**6), Pauli("III"[::-1])], 675 | [+16 / (2**6), Pauli("IZI"[::-1])], 676 | [+16 / (2**6), Pauli("ZII"[::-1])], 677 | [+16 / (2**6), Pauli("ZZI"[::-1])], 678 | [+16 / (2**6), Pauli("IIZ"[::-1])], 679 | [+16 / (2**6), Pauli("IZZ"[::-1])], 680 | [+16 / (2**6), Pauli("ZIZ"[::-1])], 681 | [+16 / (2**6), Pauli("ZZZ"[::-1])], 682 | ] 683 | else: 684 | raise ValueError("invalid or unspecified color_encoding") 685 | 686 | # devide coefficients by 2, since: 687 | # "The evolution gates are related to the Pauli rotation gates by a factor of 2" 688 | op = SparsePauliOp([item[1] for item in P], coeffs=[item[0] / 2 for item in P]) 689 | ophalf = SparsePauliOp( 690 | [item[1] for item in Phalf], coeffs=[item[0] / 2 for item in Phalf] 691 | ) 692 | return op, ophalf 693 | -------------------------------------------------------------------------------- /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 implements the Max k-Cut problem for graphs where the number of colors (k) is a power of two, using a binary encoding for node colors. It provides methods for constructing color mappings, generating quantum circuits for edges, and building the corresponding cost Hamiltonian in the Pauli basis. 20 | 21 | Attributes: 22 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 23 | k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two). 24 | method (str): The method used for circuit construction ("PauliBasis" or "Diffusion"). 25 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 26 | 27 | Methods: 28 | is_power_of_two(k): Checks if the given integer k is a power of two. 29 | validate_parameters(k, method): Validates the input parameters for k and method. 30 | construct_colors(): Constructs the mapping from binary strings to color classes based on k. 31 | create_edge_circuit(theta): Creates the parameterized quantum circuit for an edge, according to the chosen method. 32 | create_edge_circuit_fixed_node(theta): Creates the parameterized quantum circuit for an edge when one node is fixed. 33 | getPauliOperator(k_cuts, color_encoding): Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 34 | 35 | """ 36 | def __init__( 37 | self, 38 | G: nx.Graph, 39 | k_cuts: int, 40 | method: str = "Diffusion", 41 | fix_one_node: bool = False, # this fixes the last node to color 1, i.e., one qubit gets removed 42 | ) -> None: 43 | """ 44 | Args: 45 | G (nx.Graph): The input graph on which the Max k-Cut problem is defined. 46 | k_cuts (int): The number of partitions (colors) to cut the graph into (must be a power of two). 47 | method (str): The method used for circuit construction ("PauliBasis" or "Diffusion"). 48 | fix_one_node (bool): If True, fixes the last node to a specific color, reducing the number of variables. 49 | 50 | Raises: 51 | ValueError: If k_cuts is not a power of two, is less than 2, greater than 8, or if method is not valid. 52 | """ 53 | MaxKCutBinaryPowerOfTwo.validate_parameters(k_cuts, method) 54 | 55 | self.k_cuts = k_cuts 56 | self.method = method 57 | 58 | N_qubits_per_node = int(np.ceil(np.log2(self.k_cuts))) 59 | super().__init__(G, N_qubits_per_node, fix_one_node) 60 | 61 | if self.method == "PauliBasis": 62 | self.op, self.ophalf = self.getPauliOperator(self.k_cuts, "all") 63 | 64 | self.construct_colors() 65 | 66 | @staticmethod 67 | def is_power_of_two(k) -> bool: 68 | """ 69 | Checks if the given integer k is a power of two. 70 | 71 | Args: 72 | k (int): The integer to check. 73 | 74 | Returns: 75 | bool: True if k is a power of two, False otherwise. 76 | """ 77 | if k > 0 and (k & (k - 1)) == 0: 78 | return True 79 | return False 80 | 81 | @staticmethod 82 | def validate_parameters(k, method) -> None: 83 | """ 84 | Validates the input parameters for k and method. 85 | 86 | Args: 87 | k (int): Number of partitions (colors). 88 | method (str): Circuit construction method ("PauliBasis" or "Diffusion"). 89 | 90 | Raises: 91 | ValueError: If k is not a power of two. 92 | ValueError: If k is less than 2 or greater than 8. 93 | ValueError: If method is not valid. 94 | """ 95 | ### 1) k_cuts must be a power of 2 96 | if not MaxKCutBinaryPowerOfTwo.is_power_of_two(k): 97 | raise ValueError("k_cuts must be a power of two") 98 | 99 | ### 2) k_cuts needs to be between 2 and 8 100 | if (k < 2) or (k > 8): 101 | raise ValueError( 102 | "k_cuts must be 2 or more, and is not implemented for k_cuts > 8" 103 | ) 104 | 105 | ### 3) method 106 | valid_methods = ["PauliBasis", "Diffusion"] 107 | if method not in valid_methods: 108 | raise ValueError("method must be in " + str(valid_methods)) 109 | 110 | def construct_colors(self): 111 | """ 112 | Constructs the mapping from binary strings to color classes based on k. 113 | 114 | Raises: 115 | ValueError: If k_cuts is not supported. 116 | """ 117 | if self.k_cuts == 2: 118 | self.colors = {"color1": ["0"], "color2": ["1"]} 119 | elif self.k_cuts == 4: 120 | self.colors = { 121 | "color1": ["00"], 122 | "color2": ["01"], 123 | "color3": ["10"], 124 | "color4": ["11"], 125 | } 126 | elif self.k_cuts == 8: 127 | self.colors = { 128 | "color1": ["000"], 129 | "color2": ["001"], 130 | "color3": ["010"], 131 | "color4": ["011"], 132 | "color5": ["100"], 133 | "color6": ["101"], 134 | "color7": ["110"], 135 | "color8": ["111"], 136 | } 137 | # Create a dictionary to map each index to its corresponding set 138 | self.bitstring_to_color = {} 139 | for key, indices in self.colors.items(): 140 | for index in indices: 141 | self.bitstring_to_color[index] = key 142 | 143 | def create_edge_circuit(self, theta): 144 | """ 145 | Creates the parameterized quantum circuit for an edge, according to the chosen method. 146 | 147 | Args: 148 | theta (float): The phase parameter. 149 | 150 | Returns: 151 | qc (QuantumCircuit): The constructed quantum circuit for the edge. 152 | """ 153 | qc = QuantumCircuit(2 * self.N_qubits_per_node) 154 | if self.method == "PauliBasis": 155 | qc.append(PauliEvolutionGate(self.op, time=theta), qc.qubits) 156 | else: 157 | for k in range(self.N_qubits_per_node): 158 | qc.cx(k, self.N_qubits_per_node + k) 159 | qc.x(self.N_qubits_per_node + k) 160 | # C^{n-1}Phase 161 | if self.N_qubits_per_node == 1: 162 | phase_gate = PhaseGate(-theta) 163 | else: 164 | phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1) 165 | qc.append( 166 | phase_gate, 167 | [ 168 | self.N_qubits_per_node - 1 + ind 169 | for ind in range(1, self.N_qubits_per_node + 1) 170 | ], 171 | ) 172 | for k in reversed(range(self.N_qubits_per_node)): 173 | qc.x(self.N_qubits_per_node + k) 174 | qc.cx(k, self.N_qubits_per_node + k) 175 | return qc 176 | 177 | def create_edge_circuit_fixed_node(self, theta): 178 | """ 179 | Creates the parameterized quantum circuit for an edge when one node is fixed. 180 | 181 | Args: 182 | theta (float): The phase parameter. 183 | 184 | Returns: 185 | qc (QuantumCircuit): The constructed quantum circuit for the edge with a fixed node. 186 | """ 187 | qc = QuantumCircuit(self.N_qubits_per_node) 188 | if self.method == "PauliBasis": 189 | qc.append(PauliEvolutionGate(self.ophalf, time=-theta), qc.qubits) 190 | else: 191 | qc.x(qc.qubits) 192 | # C^{n-1}Phase 193 | if self.N_qubits_per_node == 1: 194 | phase_gate = PhaseGate(-theta) 195 | else: 196 | phase_gate = PhaseGate(-theta).control(self.N_qubits_per_node - 1) 197 | qc.append(phase_gate, qc.qubits) 198 | qc.x(qc.qubits) 199 | return qc 200 | 201 | def getPauliOperator(self, k_cuts, color_encoding): 202 | """ 203 | Returns the Pauli operators for the cost Hamiltonian for the given k and encoding. 204 | 205 | Args: 206 | k_cuts (int): Number of partitions (colors). 207 | color_encoding (str): The encoding scheme for colors. 208 | 209 | Returns: 210 | op (SparsePauliOp): The full Pauli operator for the cost Hamiltonian. 211 | ophalf (SparsePauliOp): The half Pauli operator for the fixed-node case. 212 | """ 213 | # flip Pauli strings, because of qiskit's little endian encoding 214 | if k_cuts == 2: 215 | P = [ 216 | [2 / (2**1), Pauli("ZZ")], 217 | ] 218 | Phalf = [ 219 | [2 / (2**1), Pauli("Z")], 220 | ] 221 | elif k_cuts == 4: 222 | P = [ 223 | [-8 / (2**4), Pauli("IIII"[::-1])], 224 | [+8 / (2**4), Pauli("IZIZ"[::-1])], 225 | [+8 / (2**4), Pauli("ZIZI"[::-1])], 226 | [+8 / (2**4), Pauli("ZZZZ"[::-1])], 227 | ] 228 | Phalf = [ 229 | [-8 / (2**4), Pauli("II"[::-1])], 230 | [+8 / (2**4), Pauli("IZ"[::-1])], 231 | [+8 / (2**4), Pauli("ZI"[::-1])], 232 | [+8 / (2**4), Pauli("ZZ"[::-1])], 233 | ] 234 | else: 235 | P = [ 236 | [-48 / (2**6), Pauli("IIIIII"[::-1])], 237 | [+16 / (2**6), Pauli("IIZIIZ"[::-1])], 238 | [+16 / (2**6), Pauli("IZIIZI"[::-1])], 239 | [+16 / (2**6), Pauli("IZZIZZ"[::-1])], 240 | [+16 / (2**6), Pauli("ZIIZII"[::-1])], 241 | [+16 / (2**6), Pauli("ZIZZIZ"[::-1])], 242 | [+16 / (2**6), Pauli("ZZIZZI"[::-1])], 243 | [+16 / (2**6), Pauli("ZZZZZZ"[::-1])], 244 | ] 245 | Phalf = [ 246 | [-48 / (2**6), Pauli("III"[::-1])], 247 | [+16 / (2**6), Pauli("IIZ"[::-1])], 248 | [+16 / (2**6), Pauli("IZI"[::-1])], 249 | [+16 / (2**6), Pauli("IZZ"[::-1])], 250 | [+16 / (2**6), Pauli("ZII"[::-1])], 251 | [+16 / (2**6), Pauli("ZIZ"[::-1])], 252 | [+16 / (2**6), Pauli("ZZI"[::-1])], 253 | [+16 / (2**6), Pauli("ZZZ"[::-1])], 254 | ] 255 | 256 | # devide coefficients by 2, since: 257 | # "The evolution gates are related to the Pauli rotation gates by a factor of 2" 258 | op = SparsePauliOp([item[1] for item in P], coeffs=[item[0] / 2 for item in P]) 259 | ophalf = SparsePauliOp( 260 | [item[1] for item in Phalf], coeffs=[item[0] / 2 for item in Phalf] 261 | ) 262 | return op, ophalf 263 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .statistic import Statistic 2 | from .flip import BitFlip 3 | from .post import * 4 | from .graphutils import GraphHandler 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /unittests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenQuantumComputing/QAOA/7497ae68533e6124981e6aff3364cab9114389b2/unittests/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------