├── .gitignore ├── Data noise.ipynb ├── FAQ.md ├── IBM.ipynb ├── LICENSE ├── Misc.ipynb ├── README.md ├── Round Analysis.ipynb ├── SHYPS.ipynb ├── Sliding Window GDG.ipynb ├── Sliding Window OSD.ipynb ├── Syndrome code.ipynb ├── guessing.py ├── osd.py ├── setup.py └── src ├── __init__.py ├── bp4_osd.cpp ├── bp4_osd.pxd ├── bp4_osd.pyx ├── bp_guessing_decoder.cpp ├── bp_guessing_decoder.pxd ├── bp_guessing_decoder.pyx ├── build_SHYPS_circuit.py ├── build_circuit.py ├── c_util.pxd ├── c_util.pyx ├── codes_q.py ├── include ├── COPYRIGHT ├── README.md ├── bpgd.cpp ├── bpgd.hpp ├── mod2sparse.c ├── mod2sparse.h ├── mod2sparse_extra.cpp └── mod2sparse_extra.hpp ├── mod2sparse.c ├── mod2sparse.pxd ├── mod2sparse.pyx ├── osd_window.cpp ├── osd_window.pxd ├── osd_window.pyx ├── simulation.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | build 3 | *egg-info 4 | *.c 5 | !mod2sparse.c 6 | *.o 7 | *.so 8 | __pycache__ 9 | LICENSE 10 | VERSION 11 | logs* 12 | slurm-* 13 | r00* 14 | itt-python 15 | .vscode -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | 4 | ## 1. How is your BB syndrome extraction circuit different from the [official implementation](https://github.com/sbravyi/BivariateBicycleCodes)? 5 | 6 | After uncommenting lines 145, 170, 206 from `src/build_circuit.py` you should be able to recover their results. 7 | I wrote this circuit and ran all the relevant numerics before Bravyi et al. updated their paper to version two on arXiv (and released their source code). In the author comments for arXiv v2 they said that they fixed some bugs and hence logical error rate went up. At my time of programming, I tried to match up with their first version, so I removed all possible extra error sources (idling errors and flips before transversal measurement). By extra I mean those that do not affect the circuit-level PCM, but the prior probabilities associated with some columns of the PCM. 8 | 9 | 10 | ## 2. Installation 11 | 12 | ### 2.1 How do I install the project? 13 | Currently the following is only guaranteed to work on Linux. I will try to find a solution for MAC users. 14 | ``` 15 | conda create --name gdg python=3.12 16 | conda activate gdg 17 | pip install numpy==1.26.4 cython==3.0.11 stim ldpc 18 | python setup.py build_ext --inplace 19 | ``` 20 | If no error happens, you have sucessfully install the project and can use all decoders. If you fail at the last step (cython compilation), please see the next question. 21 | 22 | If you have an NVIDIA GPU and want to benefit from the speed of GPU decoders from [CUDA-Q QEC](https://nvidia.github.io/cudaqx/quickstart/installation.html) (logical error rate is slightly worse), you can install their package via 23 | ``` 24 | pip install cudaq_qec 25 | ``` 26 | 27 | ### 2.2 My Cython compilation failed, can I still use normal BP+OSD with your sliding window framework? 28 | 29 | Yes, take `osd.py` (the script version of `Sliding Window OSD.ipynb`) as an example: 30 | - comment out `from src import osd_window` 31 | - comment out line 2-4 in `src/__init__.py` 32 | 33 | Now you can run `python osd.py` without any error. 34 | 35 | ### 2.3 I am using Linux and I managed to compile, however, my CPU is not Intel i9 13900K, can I still use GDG? 36 | 37 | Yes, you can 38 | - either by using single thread version of GDG only (toggle `multi_thread=False`). Single thread GDG (with suitable parameters) is still faster and more accurate than BP+OSD when used in window decoding. 39 | - or, if your CPU is similar, you can still try running multi-thread GDG; if it does not work, you can change the thread to core assignment in line 605 and 612 of `/src/include/bpgd.cpp` (please read through `Data noise.ipynb` before doing so). 40 | 41 | ### 2.4 How did you measure the worst-case latency of GDG? 42 | As claimed in the abstract of our paper, the worst-case latency is around 3ms. This is demonstrated in `Sliding Window GDG.ipynb`. Please note that we only measure the *theoretical* worst-case latency, namely those samples that GDG failed to converge on. Otherwise, there would be occasional spikes at 10ms, this is a problem with all CPU decoders; it is also encounted in the [Google's surface code experiments](https://arxiv.org/pdf/2408.13687) Figure S3. I think this is an OS issue and though I linked to [this post](https://shuhaowu.com/blog/2022/01-linux-rt-appdev-part1.html), I did not solve it myself. 43 | 44 | Switching to GPU could be a solution. In my latency probing of the CUDA BP decoder, I did not observe such spikes, therefore I plan to implement a CUDA version of GDG. I should say beforehand that I don't expect the *theoretical* worst-case latency to improve much, since 200 iterations of CUDA BP takes 2ms on my RTX4090. 45 | 46 | ## 3. Heuristics 47 | 48 | ### 3.1 How do you tune BP+OSD parameters? 49 | 50 | For the scaling factor, on code-capacity noise (data qubit noise), I try all four values from {0.5, 0.625, 0.8, 1.0} and pick the best one. For BB codes, see Fig. 4 from our paper for the best scaling factors. For circuit-level noise, from my experience $1.0$ is okay, probably because the Tanner graph is highly-irregular. 51 | 52 | For BP iterations, I just set it to 100~200 for code-capcity noise without any tuning; I think a suitable scaling factor is more important there. However, for circuit-level noise, since I was doing the shortening trick to the wide circuit-level PCM (see `Sliding Window OSD.ipynb` for details) before running GDG, I found that running BP for too many iterations before shortening causes error floor. 53 | 54 | ### 3.2 What are the main features of GDG? 55 | 56 | GDG is an ensembled BP-based decoder and there is no dependency within the ensemble. Moreover, it is iterative and each time it chooses the most-likely-to-flip VN to decimate. Each agent within the ensemble either decimates that VN according to the sign of the BP posterior LLR, or contrary to that, this is prefixed for each agent. 57 | 58 | ### 3.3 If I want to design other decoders, what heuristics can be transferred there? 59 | 60 | - One could use LLR history to perform decisions, in our paper, we mostly only used the sum of the history, maybe better criterions can be devised. 61 | - The shortening trick for circuit-level noise could be widely applicable. 62 | 63 | 64 | --- 65 | 66 | *Still have questions? Feel free to contact me at gonga@student.ethz.ch.* 67 | -------------------------------------------------------------------------------- /IBM.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "id": "13e161c0", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [ 13 | "1.15.0\n", 14 | "BB_n144_k12\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "%reload_ext autoreload\n", 20 | "%autoreload 2\n", 21 | "%matplotlib inline\n", 22 | "import stim\n", 23 | "print(stim.__version__)\n", 24 | "\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import numpy as np\n", 27 | "import math\n", 28 | "\n", 29 | "from ldpc import BpDecoder, BpOsdDecoder\n", 30 | "import time\n", 31 | "from src.utils import rank\n", 32 | "from src.codes_q import create_bivariate_bicycle_codes, create_circulant_matrix\n", 33 | "from src.build_circuit import build_circuit, dem_to_check_matrices\n", 34 | "from src import osd_window # comment this out if you only want to use normal BP+OSD, or your cython compilation failed\n", 35 | "\n", 36 | "# [[72,12,6]]\n", 37 | "# code, A_list, B_list = create_bivariate_bicycle_codes(6, 6, [3], [1,2], [1,2], [3])\n", 38 | "# d = 6\n", 39 | "\n", 40 | "# [[90,8,10]]\n", 41 | "# code, A_list, B_list = create_bivariate_bicycle_codes(15, 3, [9], [1,2], [2,7], [0])\n", 42 | "# d = 10\n", 43 | "\n", 44 | "# [[108,8,10]]\n", 45 | "# code, A_list, B_list = create_bivariate_bicycle_codes(9, 6, [3], [1,2], [1,2], [3])\n", 46 | "# d = 10\n", 47 | "\n", 48 | "# [[144,12,12]]\n", 49 | "code, A_list, B_list = create_bivariate_bicycle_codes(12, 6, [3], [1,2], [1,2], [3])\n", 50 | "d = 12\n", 51 | "\n", 52 | "# [[288,12,18]]\n", 53 | "# code, A_list, B_list = create_bivariate_bicycle_codes(12, 12, [3], [2,7], [1,2], [3])\n", 54 | "# d = 18\n", 55 | "\n", 56 | "# [[360,12,<=24]]\n", 57 | "# code, A_list, B_list = create_bivariate_bicycle_codes(30, 6, [9], [1,2], [25,26], [3])\n", 58 | "\n", 59 | "# [[756,16,<=34]]\n", 60 | "# code, A_list, B_list = create_bivariate_bicycle_codes(21,18, [3], [10,17], [3,19], [5])\n", 61 | "\n", 62 | "print(code.name)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "id": "7be87e9a", 68 | "metadata": {}, 69 | "source": [ 70 | "[1] High-threshold and low-overhead fault-tolerant quantum memory (**arXiv v1**)\n", 71 | "\n", 72 | "This notebook tries to reproduce the result in [1] Figure 3. Please note that figure numbers change in their arXiv v2, as well as the logical error rates (see FAQ question 1).\n", 73 | "\n", 74 | "Toggle the code above and change the `num_repeat` parameter to the code distance in the `build_circuit` function below. This function implements the circuit in Figure 7 / Table 5 of [1].\n", 75 | "\n", 76 | "Following the Stim style of circuit creation:\n", 77 | "- all data qubits are initialized into $|0\\rangle$ if in the z-basis or $|+\\rangle$ if in the x-basis.\n", 78 | "- the first round is an encoding round (using the same syndrome measurement circuit), detectors are put onto the z-checks if in the z-basis or onto x-checks if in the x-basis. Detectors can NOT be put onto both types of checks in the encoding round.\n", 79 | "- afterwards, syndrome measurement rounds are repeated `num_repeat-1` times, detectors are the XORed results of current z/x-checks and those from the previous round if in the z/x-basis. Detectors can be put onto both types of checks when setting `use_both` to `True`. It is not recommended to do so, as the detector error model check matrix will otherwise be too large. In other words, only independent X and Z decoding (`use_both=False`) is implemented in this repo.\n", 80 | "- the data qubits are directly measured in the z/x-basis, and a noiseless syndrome measurement results are calculated using these data qubit measurement results. Again, these results are XORed with the previous round. This mimics the behavior of memory experiments (cf. surface code). This round is a simulation trick, as data qubits are never collapsed in real-time experiments (e.g. before a non-Clifford gate). An alternative implementation of this round is to still use syndrome measurement circuit but remove noise from CNOT gates.\n", 81 | "- observables are the logical operators.\n", 82 | "- Currently only one identity gate on L/R data is implemented as before-round-depolarization, performance does not accord with [1] if put two (cf. Table 5 [1]). Ancilla measurement and re-initialization is merged into `MR` in Stim.\n", 83 | "\n", 84 | "The fault mechanism combination described in Section 6 of [1] is handled by Stim `detector_error_model` and the glue code `dem_to_check_matrices` was adapted from PyMatching.\n" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 5, 90 | "id": "4d7a2608", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def decode(p, num_repeat, num_shots=10000, osd_order=10, shorten=False): # whether use my heuristic OSD on shortened PCM)\n", 95 | " circuit = build_circuit(code, A_list, B_list, \n", 96 | " p=p, # physical error rate\n", 97 | " num_repeat=num_repeat, # usually set to code distance\n", 98 | " z_basis=True, # whether in the z-basis or x-basis\n", 99 | " use_both=False, # whether use measurement results in both basis to decode one basis\n", 100 | " )\n", 101 | " dem = circuit.detector_error_model()\n", 102 | " chk, obs, priors, col_dict = dem_to_check_matrices(dem, return_col_dict=True)\n", 103 | " num_row, num_col = chk.shape\n", 104 | " chk_row_wt = np.sum(chk, axis=1)\n", 105 | " chk_col_wt = np.sum(chk, axis=0)\n", 106 | " print(f\"check matrix shape {chk.shape}, max (row, column) weight ({np.max(chk_row_wt)}, {np.max(chk_col_wt)}),\",\n", 107 | " f\"min (row, column) weight ({np.min(chk_row_wt)}, {np.min(chk_col_wt)})\")\n", 108 | " if not shorten:\n", 109 | " bpd = BpOsdDecoder(\n", 110 | " chk, # the parity check matrix\n", 111 | " channel_probs=list(priors), # assign error_rate to each VN\n", 112 | " max_iter=10000, # the maximum number of iterations for BP\n", 113 | " bp_method=\"minimum_sum\", # messages are not clipped, may have numerical issues\n", 114 | " ms_scaling_factor=1.0, # min sum scaling factor. If set to zero, the variable scaling factor method is used\n", 115 | " osd_method=\"OSD_CS\", # the OSD method. Choose from: 1) \"osd_e\", \"osd_cs\", \"osd0\"\n", 116 | " osd_order=osd_order, # the osd search depth, not specified in v1, later specified to be 7 in their arXiv v2\n", 117 | " )\n", 118 | " else: # see Sliding Window OSD.ipynb for more detail\n", 119 | " bpd = osd_window(\n", 120 | " chk,\n", 121 | " channel_probs=priors,\n", 122 | " pre_max_iter=16, # BP preprocessing on original PCM\n", 123 | " post_max_iter=1000, # BP on shortened PCM\n", 124 | " ms_scaling_factor=1.0,\n", 125 | " new_n=None, # if set to None, 2*num_row columns will be kept\n", 126 | " osd_method=\"osd_cs\",\n", 127 | " osd_order=10\n", 128 | " )\n", 129 | "\n", 130 | " start_time = time.perf_counter()\n", 131 | " dem_sampler: stim.CompiledDemSampler = dem.compile_sampler()\n", 132 | " det_data, obs_data, err_data = dem_sampler.sample(shots=num_shots, return_errors=False, bit_packed=False)\n", 133 | " print(\"detector data shape\", det_data.shape)\n", 134 | " print(\"observable data shape\", obs_data.shape)\n", 135 | " end_time = time.perf_counter()\n", 136 | " print(f\"Stim: noise sampling for {num_shots} shots, elapsed time:\", end_time-start_time)\n", 137 | "\n", 138 | " num_err = 0\n", 139 | " num_flag_err = 0\n", 140 | " start_time = time.perf_counter()\n", 141 | " for i in range(num_shots):\n", 142 | " e_hat = bpd.decode(det_data[i])\n", 143 | " num_flag_err += ((chk @ e_hat + det_data[i]) % 2).any()\n", 144 | " ans = (obs @ e_hat + obs_data[i]) % 2\n", 145 | " num_err += ans.any()\n", 146 | " end_time = time.perf_counter()\n", 147 | " print(\"Elapsed time:\", end_time-start_time)\n", 148 | " print(f\"Flagged Errors: {num_flag_err}/{num_shots}\") # expect 0 for OSD\n", 149 | " print(f\"Logical Errors: {num_err}/{num_shots}\")\n", 150 | " p_l = num_err / num_shots\n", 151 | " p_l_per_round = 1-(1-p_l) ** (1/num_repeat)\n", 152 | " print(\"Logical error per round:\", p_l_per_round)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 382, 158 | "id": "e33d0efe", 159 | "metadata": {}, 160 | "outputs": [ 161 | { 162 | "name": "stdout", 163 | "output_type": "stream", 164 | "text": [ 165 | "check matrix shape (936, 8784), max (row, column) weight (35, 6), min (row, column) weight (16, 2)\n", 166 | "detector data shape (10000, 936)\n", 167 | "observable data shape (10000, 12)\n", 168 | "Stim: noise sampling for 10000 shots, elapsed time: 0.018485471606254578\n", 169 | "Elapsed time: 1657.7230640873313\n", 170 | "Flagged Errors: 0/10000\n", 171 | "Logical Errors: 76/10000\n", 172 | "Logical error per round: 0.0006355502160568793\n" 173 | ] 174 | } 175 | ], 176 | "source": [ 177 | "decode(p=0.004, num_repeat=d, num_shots=10000)" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": 384, 183 | "id": "55f61110", 184 | "metadata": {}, 185 | "outputs": [ 186 | { 187 | "name": "stdout", 188 | "output_type": "stream", 189 | "text": [ 190 | "check matrix shape (936, 8784), max (row, column) weight (35, 6), min (row, column) weight (16, 2)\n", 191 | "detector data shape (100000, 936)\n", 192 | "observable data shape (100000, 12)\n", 193 | "Stim: noise sampling for 100000 shots, elapsed time: 0.2078531989827752\n", 194 | "Elapsed time: 4199.123198093846\n", 195 | "Flagged Errors: 0/100000\n", 196 | "Logical Errors: 77/100000\n", 197 | "Logical error per round: 6.418932329932403e-05\n" 198 | ] 199 | } 200 | ], 201 | "source": [ 202 | "decode(p=0.003, num_repeat=d, num_shots=100000)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 6, 208 | "id": "cc3cbd1d", 209 | "metadata": {}, 210 | "outputs": [ 211 | { 212 | "name": "stdout", 213 | "output_type": "stream", 214 | "text": [ 215 | "check matrix shape (936, 8784), max (row, column) weight (35, 6), min (row, column) weight (16, 2)\n", 216 | "detector data shape (10000, 936)\n", 217 | "observable data shape (10000, 12)\n", 218 | "Stim: noise sampling for 10000 shots, elapsed time: 0.023050088435411453\n", 219 | "Elapsed time: 428.5726739112288\n", 220 | "Flagged Errors: 0/10000\n", 221 | "Logical Errors: 90/10000\n", 222 | "Logical error per round: 0.0007531116566323881\n" 223 | ] 224 | } 225 | ], 226 | "source": [ 227 | "decode(p=0.004, num_repeat=d, num_shots=10000, shorten=True)" 228 | ] 229 | } 230 | ], 231 | "metadata": { 232 | "kernelspec": { 233 | "display_name": "Python 3 (ipykernel)", 234 | "language": "python", 235 | "name": "python3" 236 | }, 237 | "language_info": { 238 | "codemirror_mode": { 239 | "name": "ipython", 240 | "version": 3 241 | }, 242 | "file_extension": ".py", 243 | "mimetype": "text/x-python", 244 | "name": "python", 245 | "nbconvert_exporter": "python", 246 | "pygments_lexer": "ipython3", 247 | "version": "3.12.9" 248 | } 249 | }, 250 | "nbformat": 4, 251 | "nbformat_minor": 5 252 | } 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anqi Gong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sliding Window with Guided Decimation Guessing (GDG) Decoding 2 | 3 | This repo contains the source codes of the paper [Toward Low-latency Iterative Decoding of QLDPC Codes Under Circuit-Level Noise](https://arxiv.org/pdf/2403.18901.pdf). 4 | 5 | Update [May-13-2025] 6 | - [x] fix typos, update BP+OSD import/calls to ldpc_v2, add [CUDA-Q QEC](https://nvidia.github.io/cudaqx/components/qec/introduction.html) GPU decoder into `Sliding Window OSD.ipynb` 7 | - [x] [SHYPS](https://arxiv.org/pdf/2502.07150) memory experiment in `SHPYS.ipynb` 8 | - [x] FAQ page 9 | - [ ] make sliding window part more reusable 10 | - [ ] update the BP part from GDG to LDPC v2, improve code-capacity noise multi-thread GDG implementation 11 | - [ ] let MAC users install from the directory 12 | - [ ] CUDA implementation of GDG 13 | - [ ] [logical Clifford synthesis](https://github.com/gongaa/RM127/blob/main/efficiency.pdf) compiler 14 | - [ ] SHYPS logical experiment 15 | 16 | 17 | Please checkout the FAQ page for installation! 18 | 19 | ## Notebooks 20 | 21 | - `IBM.ipynb` aims to reproduce the results in Figure 3 of the [IBM paper arXiv v1](https://arxiv.org/pdf/2308.07915v1.pdf) (they updated their numerical results in v2). `N72circuit.svg` is the visualization of the noiseless, single syndrome cycle Stim circuit of their Figure 7 for the [[72,12,6]] code. 22 | - `Round Analysis.ipynb` takes a closer look at the parity check matrix, explains the basic concept of sliding window decoding and show how the windows are extracted. 23 | - `Sliding Window OSD.ipynb` contains the complete pipleline of code/circuit creation, window extraction and BP+OSD is used to decode each window. 24 | - `Data noise.ipynb` introduces basic usage of GDG, and is for producing Figure 4 of our paper. 25 | - `Sliding Window GDG.ipynb` uses GDG on each window for decoding, for Figure 3 and 7. 26 | - `Syndrome code.ipynb` is related to Appendix B. 27 | - `Misc.ipynb` is not related to our paper, but demonstrates my implementation of the following papers: [BP4+OSD](https://quantum-journal.org/papers/q-2021-11-22-585/pdf/), [2BGA codes](https://arxiv.org/pdf/2306.16400.pdf), [CAMEL](https://arxiv.org/pdf/2401.06874.pdf), [BPGD](https://arxiv.org/pdf/2312.10950.pdf). 28 | 29 | ## Directory Layout 30 | src 31 | ├── include 32 | │ └── bpgd.cpp # base class for handling decimation, multi-thread GDG 33 | │ 34 | ├── bp_guessing_decoder.pyx # single-thread GDG, it shares interface with the multi-thread version 35 | ├── osd_window.pyx # OSD on shortened window PCM 36 | ├── codes_q.py # code constructions 37 | ├── build_circuit.py # build Stim circuit for BB codes 38 | └── simulation.py # simulation framework for data qubit noise -------------------------------------------------------------------------------- /guessing.py: -------------------------------------------------------------------------------- 1 | import stim 2 | print(stim.__version__) 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import math 7 | 8 | from ldpc import BpDecoder, BpOsdDecoder 9 | import time 10 | from src.utils import rank 11 | from src.codes_q import create_bivariate_bicycle_codes, create_circulant_matrix 12 | from src.build_circuit import build_circuit, dem_to_check_matrices 13 | from src import bpgdg_decoder 14 | # import itt # install see https://github.com/oleksandr-pavlyk/itt-python, you also need to install VTune 15 | 16 | hard_samples = [] 17 | 18 | decoding_time = [] 19 | def sliding_window_decoder(N, p=0.003, num_repeat=12, num_shots=10000, max_iter=200, W=3, F=1, z_basis=True, 20 | noisy_prior=None, method=1, plot=False, low_error_mode=False, 21 | max_step=25, max_iter_per_step=6, max_tree_depth=3, max_side_depth=10, max_side_branch_step=10, 22 | last_win_gdg_factor=1.0, last_win_bp_factor=1.0): 23 | 24 | if N == 72: 25 | code, A_list, B_list = create_bivariate_bicycle_codes(6, 6, [3], [1,2], [1,2], [3]) # 72 26 | elif N == 90: 27 | code, A_list, B_list = create_bivariate_bicycle_codes(15, 3, [9], [1,2], [2,7], [0]) # 90 28 | elif N == 108: 29 | code, A_list, B_list = create_bivariate_bicycle_codes(9, 6, [3], [1,2], [1,2], [3]) # 108 30 | elif N == 144: 31 | code, A_list, B_list = create_bivariate_bicycle_codes(12, 6, [3], [1,2], [1,2], [3]) # 144 32 | elif N == 288: 33 | code, A_list, B_list = create_bivariate_bicycle_codes(12, 12, [3], [2,7], [1,2], [3]) # 288 34 | elif N == 360: 35 | code, A_list, B_list = create_bivariate_bicycle_codes(30, 6, [9], [1,2], [25,26], [3]) # 360 36 | elif N == 756: 37 | code, A_list, B_list = create_bivariate_bicycle_codes(21,18, [3], [10,17], [3,19], [5]) # 756 38 | else: 39 | print("unsupported N") 40 | return 41 | 42 | circuit = build_circuit(code, A_list, B_list, p, num_repeat, z_basis=z_basis) 43 | dem = circuit.detector_error_model() 44 | chk, obs, priors, col_dict = dem_to_check_matrices(dem, return_col_dict=True) 45 | num_row, num_col = chk.shape 46 | n = code.N 47 | n_half = n//2 48 | 49 | lower_bounds = [] 50 | upper_bounds = [] 51 | i = 0 52 | while i < num_row: 53 | lower_bounds.append(i) 54 | upper_bounds.append(i+n_half) 55 | if i+n > num_row: 56 | break 57 | lower_bounds.append(i) 58 | upper_bounds.append(i+n) 59 | i += n_half 60 | 61 | region_dict = {} 62 | for i, (l,u) in enumerate(zip(lower_bounds, upper_bounds)): 63 | region_dict[(l,u)] = i 64 | 65 | region_cols = [[] for _ in range(len(region_dict))] 66 | 67 | for i in range(num_col): 68 | nnz_col = np.nonzero(chk[:,i])[0] 69 | l = nnz_col.min() // n_half * n_half 70 | u = (nnz_col.max() // n_half + 1) * n_half 71 | region_cols[region_dict[(l,u)]].append(i) 72 | 73 | chk = np.concatenate([chk[:,col].toarray() for col in region_cols], axis=1) 74 | obs = np.concatenate([obs[:,col].toarray() for col in region_cols], axis=1) 75 | priors = np.concatenate([priors[col] for col in region_cols]) 76 | 77 | anchors = [] 78 | j = 0 79 | for i in range(num_col): 80 | nnz_col = np.nonzero(chk[:,i])[0] 81 | if (nnz_col.min() >= j): 82 | anchors.append((j, i)) 83 | j += n_half 84 | anchors.append((num_row, num_col)) 85 | 86 | if noisy_prior is None and method != 0: 87 | b = anchors[W] 88 | c = anchors[W-1] 89 | if method == 1: 90 | c = (c[0], c[1]+n_half*3) # try also this for x basis 91 | noisy_prior = np.sum(chk[c[0]:b[0],c[1]:b[1]] * priors[c[1]:b[1]], axis=1) 92 | print("prior for noisy syndrome", noisy_prior[0]) 93 | 94 | if method != 0: 95 | noisy_syndrome_priors = np.ones(n_half) * noisy_prior 96 | 97 | num_win = math.ceil((len(anchors)-W+F-1) / F) 98 | chk_submats = [] 99 | prior_subvecs = [] 100 | if plot: 101 | fig, ax = plt.subplots(num_win, 1) 102 | top_left = 0 103 | 104 | for i in range(num_win): 105 | a = anchors[top_left] 106 | bottom_right = min(top_left + W, len(anchors)-1) 107 | b = anchors[bottom_right] 108 | 109 | if i != num_win-1 and method != 0: # not the last round 110 | c = anchors[top_left + W - 1] 111 | if method == 1: 112 | c = (c[0], c[1]+n_half*3) # try also this for x basis 113 | noisy_syndrome = np.zeros((n_half*W,n_half)) 114 | noisy_syndrome[-n_half:,:] = np.eye(n_half)# * noisy_syndrome_prior 115 | mat = chk[a[0]:b[0],a[1]:c[1]] 116 | mat = np.hstack((mat, noisy_syndrome)) 117 | prior = priors[a[1]:c[1]] 118 | prior = np.concatenate((prior, noisy_syndrome_priors)) 119 | else: # method==0 or last round 120 | mat = chk[a[0]:b[0],a[1]:b[1]] 121 | prior = priors[a[1]:b[1]] 122 | chk_submats.append(mat) 123 | prior_subvecs.append(prior) 124 | if plot: 125 | ax[i].imshow(mat, cmap="gist_yarg") 126 | top_left += F 127 | 128 | start_time = time.perf_counter() 129 | dem_sampler: stim.CompiledDemSampler = dem.compile_sampler() 130 | det_data, obs_data, err_data = dem_sampler.sample(shots=num_shots, return_errors=False, bit_packed=False) 131 | end_time = time.perf_counter() 132 | print(f"Stim: noise sampling for {num_shots} shots, elapsed time:", end_time-start_time) 133 | 134 | 135 | total_e_hat = np.zeros((num_shots,num_col)) 136 | new_det_data = det_data.copy() 137 | start_time = time.perf_counter() 138 | top_left = 0 139 | i = 0 140 | osd = False 141 | while i < num_win: 142 | mat = chk_submats[i] 143 | prior = prior_subvecs[i] 144 | a = anchors[top_left] 145 | bottom_right = min(top_left + W, len(anchors)-1) 146 | b = anchors[bottom_right] 147 | c = anchors[top_left+F] # commit region bottom right 148 | 149 | if i==num_win-1 and osd: 150 | bpd = BpOsdDecoder( 151 | mat, 152 | channel_probs=list(prior), 153 | max_iter=200, 154 | bp_method="minimum_sum", 155 | ms_scaling_factor=1.0, 156 | osd_method="OSD_CS", 157 | osd_order=10, 158 | ) 159 | else: 160 | bpgdg = bpgdg_decoder( 161 | mat, 162 | channel_probs=prior, 163 | max_iter=max_iter, 164 | max_iter_per_step=max_iter_per_step, 165 | max_step=max_step, 166 | max_tree_depth=max_tree_depth, 167 | max_side_depth=max_side_depth, 168 | max_tree_branch_step=max_side_branch_step, 169 | max_side_branch_step=max_side_branch_step, 170 | multi_thread=True, 171 | low_error_mode=low_error_mode, 172 | gdg_factor=last_win_gdg_factor if (i==num_win-1) else 1.0, 173 | ms_scaling_factor=last_win_bp_factor if (i==num_win-1) else 1.0, 174 | ) 175 | num_flag_err = 0 176 | # if i==num_win - 1: # after gathering hard sample, uncomment these two lines 177 | # return mat, prior # to get mat and prior for the last window 178 | detector_win = new_det_data[:,a[0]:b[0]] 179 | llr_prior = np.log((1.0-prior)/prior) 180 | sum_wt = 0 181 | for j in range(num_shots): 182 | if i==num_win-1 and osd: 183 | e_hat = bpd.decode(detector_win[j]) 184 | is_flagged = ((mat @ e_hat + detector_win[j]) % 2).any() 185 | else: 186 | # e_hat_osd = bpd.decode(detector_win[j]) 187 | decoding_start_time = time.perf_counter() 188 | # itt.resume() 189 | e_hat = bpgdg.decode(detector_win[j]) 190 | # itt.pause() 191 | 192 | # pm_osd = llr_prior[e_hat_osd.astype(bool)].sum() 193 | # pm_gdg = llr_prior[e_hat.astype(bool)].sum() 194 | # if pm_osd != pm_gdg: 195 | # print(f"osd pm {pm_osd}, gdg pm {pm_gdg}") 196 | decoding_end_time = time.perf_counter() 197 | is_flagged = 1 - bpgdg.converge 198 | if is_flagged: decoding_time.append(decoding_end_time-decoding_start_time) 199 | 200 | # if is_flagged and i==num_win-1: 201 | # hard_samples.append(detector_win[j]) 202 | sum_wt += e_hat.sum() 203 | num_flag_err += is_flagged 204 | if i == num_win-1: # last window 205 | total_e_hat[j][a[1]:b[1]] = e_hat 206 | else: 207 | total_e_hat[j][a[1]:c[1]] = e_hat[:c[1]-a[1]] 208 | 209 | print(f"Window {i}, average weight {sum_wt/num_shots}") 210 | print(f"Window {i}, flagged Errors: {num_flag_err}/{num_shots}") 211 | 212 | if i!=num_win - 1: 213 | new_det_data = (det_data + total_e_hat @ chk.T) % 2 214 | top_left += F 215 | else: 216 | end_time = time.perf_counter() 217 | print("Elapsed time:", end_time-start_time) 218 | print("last round osd", osd) 219 | flagged_err = ((det_data + total_e_hat @ chk.T) % 2).any(axis=1) 220 | num_flagged_err = flagged_err.astype(int).sum() 221 | print(f"Overall Flagged Errors: {num_flagged_err}/{num_shots}") 222 | logical_err = ((obs_data + total_e_hat @ obs.T) % 2).any(axis=1) 223 | num_err = np.logical_or(flagged_err, logical_err).astype(int).sum() 224 | print(f"Logical Errors: {num_err}/{num_shots}") 225 | p_l = num_err / num_shots 226 | p_l_per_round = 1-(1-p_l) ** (1/num_repeat) 227 | print("logical error per round:", p_l_per_round) 228 | 229 | if i == num_win-1 and osd: 230 | break 231 | 232 | if i == num_win-1 and (not osd): 233 | i -= 1 234 | osd = True 235 | 236 | i += 1 237 | 238 | 239 | sliding_window_decoder(N=144, p=0.005, num_repeat=4, W=3, F=1, num_shots=5000, max_iter=8, method=1, z_basis=True) -------------------------------------------------------------------------------- /osd.py: -------------------------------------------------------------------------------- 1 | import stim 2 | print(stim.__version__) 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import math 7 | 8 | from ldpc import BpDecoder, BpOsdDecoder 9 | import time 10 | from src.utils import rank 11 | from src.codes_q import create_bivariate_bicycle_codes, create_circulant_matrix 12 | from src.build_circuit import build_circuit, dem_to_check_matrices 13 | from src import osd_window 14 | 15 | def sliding_window_decoder(N, p=0.003, num_repeat=12, num_shots=10000, max_iter=200, W=2, F=1, z_basis=True, noisy_prior=None, method=0, plot=False, shorten=False): 16 | 17 | if N == 72: 18 | code, A_list, B_list = create_bivariate_bicycle_codes(6, 6, [3], [1,2], [1,2], [3]) # 72 19 | elif N == 90: 20 | code, A_list, B_list = create_bivariate_bicycle_codes(15, 3, [9], [1,2], [2,7], [0]) # 90 21 | elif N == 108: 22 | code, A_list, B_list = create_bivariate_bicycle_codes(9, 6, [3], [1,2], [1,2], [3]) # 108 23 | elif N == 144: 24 | code, A_list, B_list = create_bivariate_bicycle_codes(12, 6, [3], [1,2], [1,2], [3]) # 144 25 | elif N == 288: 26 | code, A_list, B_list = create_bivariate_bicycle_codes(12, 12, [3], [2,7], [1,2], [3]) # 288 27 | elif N == 360: 28 | code, A_list, B_list = create_bivariate_bicycle_codes(30, 6, [9], [1,2], [25,26], [3]) # 360 29 | elif N == 756: 30 | code, A_list, B_list = create_bivariate_bicycle_codes(21,18, [3], [10,17], [3,19], [5]) # 756 31 | else: 32 | print("unsupported N") 33 | return 34 | 35 | circuit = build_circuit(code, A_list, B_list, p, num_repeat, z_basis=z_basis) 36 | dem = circuit.detector_error_model() 37 | chk, obs, priors, col_dict = dem_to_check_matrices(dem, return_col_dict=True) 38 | num_row, num_col = chk.shape 39 | n = code.N 40 | n_half = n//2 41 | 42 | lower_bounds = [] 43 | upper_bounds = [] 44 | i = 0 45 | while i < num_row: 46 | lower_bounds.append(i) 47 | upper_bounds.append(i+n_half) 48 | if i+n > num_row: 49 | break 50 | lower_bounds.append(i) 51 | upper_bounds.append(i+n) 52 | i += n_half 53 | 54 | region_dict = {} 55 | for i, (l,u) in enumerate(zip(lower_bounds, upper_bounds)): 56 | region_dict[(l,u)] = i 57 | 58 | region_cols = [[] for _ in range(len(region_dict))] 59 | 60 | for i in range(num_col): 61 | nnz_col = np.nonzero(chk[:,i])[0] 62 | l = nnz_col.min() // n_half * n_half 63 | u = (nnz_col.max() // n_half + 1) * n_half 64 | region_cols[region_dict[(l,u)]].append(i) 65 | 66 | chk = np.concatenate([chk[:,col].toarray() for col in region_cols], axis=1) 67 | obs = np.concatenate([obs[:,col].toarray() for col in region_cols], axis=1) 68 | priors = np.concatenate([priors[col] for col in region_cols]) 69 | 70 | anchors = [] 71 | j = 0 72 | for i in range(num_col): 73 | nnz_col = np.nonzero(chk[:,i])[0] 74 | if (nnz_col.min() >= j): 75 | anchors.append((j, i)) 76 | j += n_half 77 | anchors.append((num_row, num_col)) 78 | 79 | if noisy_prior is None and method != 0: 80 | b = anchors[W] 81 | c = anchors[W-1] 82 | if method == 1: 83 | c = (c[0], c[1]+n_half*3) if z_basis else (c[0], c[1]+n) 84 | # c = (c[0], c[1]+n_half*3) # try also this for x basis 85 | noisy_prior = np.sum(chk[c[0]:b[0],c[1]:b[1]] * priors[c[1]:b[1]], axis=1) 86 | print("prior for noisy syndrome", noisy_prior[0]) 87 | 88 | if method != 0: 89 | noisy_syndrome_priors = np.ones(n_half) * noisy_prior 90 | 91 | num_win = math.ceil((len(anchors)-W+F-1) / F) 92 | chk_submats = [] 93 | prior_subvecs = [] 94 | if plot: 95 | fig, ax = plt.subplots(num_win, 1) 96 | top_left = 0 97 | i = 0 98 | for i in range(num_win): 99 | a = anchors[top_left] 100 | bottom_right = min(top_left + W, len(anchors)-1) 101 | b = anchors[bottom_right] 102 | 103 | if i != num_win-1 and method != 0: # not the last round 104 | c = anchors[top_left + W - 1] 105 | if method == 1: 106 | c = (c[0], c[1]+n_half*3) if z_basis else (c[0], c[1]+n) 107 | # c = (c[0], c[1]+n_half*3) # try also this for x basis 108 | noisy_syndrome = np.zeros((n_half*W,n_half)) 109 | noisy_syndrome[-n_half:,:] = np.eye(n_half)# * noisy_syndrome_prior 110 | mat = chk[a[0]:b[0],a[1]:c[1]] 111 | mat = np.hstack((mat, noisy_syndrome)) 112 | prior = priors[a[1]:c[1]] 113 | prior = np.concatenate((prior, noisy_syndrome_priors)) 114 | else: # method==0 or last round 115 | mat = chk[a[0]:b[0],a[1]:b[1]] 116 | prior = priors[a[1]:b[1]] 117 | chk_submats.append(mat) 118 | prior_subvecs.append(prior) 119 | if plot: 120 | ax[i].imshow(mat, cmap="gist_yarg") 121 | top_left += F 122 | 123 | start_time = time.perf_counter() 124 | dem_sampler: stim.CompiledDemSampler = dem.compile_sampler() 125 | det_data, obs_data, err_data = dem_sampler.sample(shots=num_shots, return_errors=False, bit_packed=False) 126 | end_time = time.perf_counter() 127 | print(f"Stim: noise sampling for {num_shots} shots, elapsed time:", end_time-start_time) 128 | 129 | 130 | total_e_hat = np.zeros((num_shots,num_col)) 131 | new_det_data = det_data.copy() 132 | start_time = time.perf_counter() 133 | top_left = 0 134 | for i in range(num_win): 135 | mat = chk_submats[i] 136 | prior = prior_subvecs[i] 137 | a = anchors[top_left] 138 | bottom_right = min(top_left + W, len(anchors)-1) 139 | b = anchors[bottom_right] 140 | c = anchors[top_left+F] # commit region bottom right 141 | if not shorten: 142 | bpd = BpOsdDecoder( 143 | mat, 144 | channel_probs=prior, 145 | max_iter=max_iter, 146 | bp_method="minimum_sum", 147 | ms_scaling_factor=1.0, 148 | osd_method="OSD_CS", 149 | osd_order=10, # -1 for no osd (only BP) 150 | ) 151 | else: 152 | bpd = osd_window( 153 | mat, 154 | channel_probs=prior, 155 | pre_max_iter=8, 156 | post_max_iter=max_iter, 157 | ms_scaling_factor=1.0, 158 | new_n=None, 159 | osd_method="osd_cs", 160 | osd_order=0 161 | ) 162 | 163 | num_flag_err = 0 164 | 165 | detector_win = new_det_data[:,a[0]:b[0]] 166 | for j in range(num_shots): 167 | e_hat = bpd.decode(detector_win[j]) 168 | is_flagged = ((mat @ e_hat + detector_win[j]) % 2).any() 169 | num_flag_err += is_flagged 170 | if i == num_win-1: # last window 171 | total_e_hat[j][a[1]:b[1]] = e_hat 172 | else: 173 | total_e_hat[j][a[1]:c[1]] = e_hat[:c[1]-a[1]] 174 | 175 | 176 | print(f"Window {i}, flagged Errors: {num_flag_err}/{num_shots}") 177 | 178 | new_det_data = (det_data + total_e_hat @ chk.T) % 2 179 | top_left += F 180 | 181 | end_time = time.perf_counter() 182 | print("Elapsed time:", end_time-start_time) 183 | 184 | flagged_err = ((det_data + total_e_hat @ chk.T) % 2).any(axis=1) 185 | num_flagged_err = flagged_err.astype(int).sum() 186 | print(f"Overall Flagged Errors: {num_flagged_err}/{num_shots}") 187 | logical_err = ((obs_data + total_e_hat @ obs.T) % 2).any(axis=1) 188 | num_err = np.logical_or(flagged_err, logical_err).astype(int).sum() 189 | print(f"Logical Errors: {num_err}/{num_shots}") 190 | p_l = num_err / num_shots 191 | p_l_per_round = 1-(1-p_l) ** (1/num_repeat) 192 | # may also use ** (1/(num_repeat-1)) 193 | # because the first round is for encoding, the next (num_repeat-1) rounds are syndrome measurements rounds 194 | print("logical error per round:", p_l_per_round) 195 | 196 | sliding_window_decoder(N=144, p=0.004, num_repeat=12, W=3, F=1, num_shots=100, max_iter=200, method=1, 197 | z_basis=True, shorten=False) 198 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from Cython.Build import cythonize 3 | import numpy 4 | 5 | VERSION="0.0.0" 6 | f = open("src/VERSION","w+") 7 | f.write(VERSION) 8 | f.close() 9 | 10 | extension1 = Extension( 11 | name="src.mod2sparse", 12 | sources=["src/include/mod2sparse.c", "src/mod2sparse.pyx"], 13 | libraries=[], 14 | library_dirs=[], 15 | include_dirs=[numpy.get_include(), 'src/include'], 16 | extra_compile_args=['-std=c11'] 17 | ) 18 | 19 | extension2 = Extension( 20 | name="src.c_util", 21 | sources=["src/c_util.pyx", "src/include/mod2sparse.c"], 22 | libraries=[], 23 | library_dirs=[], 24 | include_dirs=[numpy.get_include(), 'src/include'], 25 | extra_compile_args=['-std=c11'] 26 | ) 27 | 28 | extension3 = Extension( 29 | name="src.bp_guessing_decoder", 30 | sources=["src/bp_guessing_decoder.pyx", "src/include/mod2sparse.c", "src/include/bpgd.cpp"], 31 | language='c++', 32 | libraries=[], 33 | library_dirs=[], 34 | include_dirs=[numpy.get_include(), 'src/include'], 35 | extra_compile_args=['-std=c11'] 36 | ) 37 | 38 | extension4 = Extension( 39 | name="src.osd_window", 40 | sources=["src/osd_window.pyx", "src/include/mod2sparse.c", "src/include/mod2sparse_extra.cpp", "src/include/bpgd.cpp"], 41 | language='c++', 42 | libraries=[], 43 | library_dirs=[], 44 | include_dirs=[numpy.get_include(), 'src/include'], 45 | extra_compile_args=['-std=c11'] 46 | ) 47 | 48 | extension5 = Extension( 49 | name="src.bp4_osd", 50 | sources=["src/bp4_osd.pyx", "src/include/mod2sparse.c", "src/include/mod2sparse_extra.cpp", "src/include/bpgd.cpp"], 51 | language='c++', 52 | libraries=[], 53 | library_dirs=[], 54 | include_dirs=[numpy.get_include(), 'src/include'], 55 | extra_compile_args=['-std=c11'] 56 | ) 57 | 58 | setup( 59 | version=VERSION, 60 | ext_modules=cythonize([extension1, extension2, extension3, extension4, extension5]), 61 | ) 62 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .bp_guessing_decoder import bpgdg_decoder, bpgd_decoder, bp_history_decoder 3 | from .osd_window import osd_window 4 | from .bp4_osd import bp4_osd 5 | from . import __file__ 6 | 7 | def get_include(): 8 | path = os.path.dirname(__file__) 9 | return path 10 | 11 | f = open(get_include()+"/VERSION") 12 | __version__ = f.read() 13 | f.close() 14 | 15 | -------------------------------------------------------------------------------- /src/bp4_osd.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | cimport numpy as np 3 | cimport cython 4 | from libc.stdlib cimport malloc, calloc, free 5 | from libc.math cimport log, tanh, isnan, abs 6 | 7 | from .mod2sparse cimport * 8 | from .c_util cimport numpy2char, char2numpy, stackchar2numpy, numpy2double, double2numpy, spmatrix2char, numpy2int 9 | 10 | cdef extern from "bpgd.hpp": 11 | cdef void index_sort(double *soft_decisions, int *cols, int N) 12 | cdef void mod2sparse_mulvec_cpp(mod2sparse *m, char *u, char *v) 13 | cdef void mod2sparse_free_cpp(mod2sparse *m) 14 | cdef double log1pexp(double x) 15 | cdef double logaddexp(double x, double y) 16 | 17 | cdef extern from "mod2sparse_extra.hpp": 18 | cdef void mod2sparse_print_terminal (mod2sparse *A) 19 | cdef int mod2sparse_rank(mod2sparse *A) 20 | cdef mod2sparse* mod2sparse_allocate_cpp (int u, int v) 21 | cdef void mod2sparse_copycols_cpp (mod2sparse* m1, mod2sparse* m2, int* cols) 22 | cdef char* decimal_to_binary_reverse(int n, int K) 23 | 24 | cdef void LU_forward_backward_solve( 25 | mod2sparse *L, 26 | mod2sparse *U, 27 | int *rows, 28 | int *cols, 29 | char *z, 30 | char *x) 31 | 32 | cdef int mod2sparse_decomp_osd( 33 | mod2sparse *A, 34 | int R, 35 | mod2sparse *L, 36 | mod2sparse *U, 37 | int *rows, 38 | int *cols) 39 | 40 | cdef class bp4_osd: 41 | cdef mod2sparse* Hx 42 | cdef mod2sparse* Hz 43 | cdef int mx, mz, n 44 | cdef char* synd_x 45 | cdef char* synd_z 46 | cdef char* bp_decoding_synd_x 47 | cdef char* bp_decoding_synd_z 48 | cdef char* bp_decoding_x 49 | cdef char* bp_decoding_z 50 | cdef double* channel_llr_x 51 | cdef double* channel_llr_y 52 | cdef double* channel_llr_z 53 | cdef double* log_prob_ratios_x 54 | cdef double* log_prob_ratios_y 55 | cdef double* log_prob_ratios_z 56 | cdef double* prior_llr_x 57 | cdef double* prior_llr_z 58 | 59 | cdef char* current_vn 60 | cdef char* current_cn_x 61 | cdef char* current_cn_z 62 | cdef int* cols 63 | cdef double* llr_post 64 | cdef int bp_iteration, max_iter 65 | 66 | cdef double min_pm 67 | cdef int converge 68 | 69 | cdef double ms_scaling_factor 70 | cdef int MEM_ALLOCATED 71 | 72 | # OSD 73 | cdef char* osd0_decoding_x 74 | cdef char* osd0_decoding_z 75 | cdef char* osdw_decoding_x 76 | cdef char* osdw_decoding_z 77 | cdef char** osdw_encoding_inputs_x 78 | cdef char** osdw_encoding_inputs_z 79 | cdef long int encoding_input_count_x 80 | cdef long int encoding_input_count_z 81 | cdef int osd_order 82 | cdef int osd_method 83 | cdef int rank_x, rank_z 84 | cdef int kx, kz 85 | cdef int* rows_x 86 | cdef int* rows_z 87 | cdef int* orig_cols 88 | cdef int* Ht_cols_x 89 | cdef int* Ht_cols_z 90 | cdef char* y 91 | cdef char* gx 92 | cdef char* gz 93 | cdef char* Htx 94 | cdef char* Htz 95 | cdef void osd_e_setup(self) 96 | cdef void osd_cs_setup_x(self) 97 | cdef void osd_cs_setup_z(self) 98 | cdef int osd(self, basis) 99 | 100 | cpdef np.ndarray[np.int_t, ndim=2] decode(self, input_vector_x, input_vector_z) 101 | cpdef np.ndarray[np.int_t, ndim=2] camel_decode(self, input_vector_x, input_vector_z) 102 | cdef void reset(self) 103 | cdef double cal_pm(self) 104 | cdef int vn_set_value(self, vn, value) 105 | cdef int bp4_decode_llr(self) 106 | cdef void bp_init(self) 107 | cdef int vn_update(self, vn) 108 | cdef int cn_update_all(self, basis) 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/bp_guessing_decoder.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | cimport numpy as np 3 | cimport cython 4 | from libc.stdlib cimport malloc, calloc, free 5 | from libc.math cimport log, tanh, isnan, abs 6 | from .mod2sparse cimport * 7 | from .c_util cimport numpy2char, char2numpy, numpy2double, double2numpy, spmatrix2char, numpy2int 8 | 9 | cdef extern from "bpgd.hpp": 10 | cdef void index_sort(double *soft_decisions, int *cols, int N) 11 | cdef void mod2sparse_mulvec_cpp(mod2sparse *m, char *u, char *v) 12 | cdef void mod2sparse_free_cpp(mod2sparse *m) 13 | 14 | cdef cppclass BPGD: 15 | int n, m 16 | int num_active_vn 17 | mod2sparse* pcm 18 | double* llr_prior 19 | double** llr_posterior 20 | char* vn_mask 21 | char* vn_degree 22 | char* cn_mask 23 | char* cn_degree 24 | char* error 25 | char* syndrome 26 | char* temp_syndrome 27 | int num_iter 28 | double factor 29 | 30 | int min_sum_log() 31 | void init() 32 | int peel() 33 | int vn_set_value(int vn, char value) 34 | int decimate_vn_reliable(int depth, double fraction) 35 | int reset(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome) 36 | void set_masks(char* source_vn_mask, char* source_cn_mask, char* source_cn_degree) 37 | double get_pm() 38 | BPGD(int m, int n, int num_iter, int low_error_mode, double factor) except + 39 | BPGD() 40 | 41 | cdef cppclass BPGD_main_thread(BPGD): 42 | char* min_pm_error 43 | double min_pm 44 | void do_work(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome); 45 | BPGD_main_thread(int m, int n, int low_error_mode) except + 46 | BPGD_main_thread(int m, int n, int num_iter, int max_step, int max_tree_depth, int max_side_depth, int max_tree_step, int max_side_step, int low_error_mode, double factor) except + 47 | BPGD_main_thread() 48 | 49 | cdef class bp_history_decoder: 50 | cdef mod2sparse* H 51 | cdef int m, n 52 | cdef char* synd 53 | cdef char* bp_decoding_synd 54 | cdef char* bp_decoding 55 | cdef double* channel_llr 56 | cdef double** log_prob_ratios 57 | cdef int history_length, bp_iteration 58 | cdef int converge 59 | cdef int max_iter 60 | cdef double ms_scaling_factor 61 | cdef int MEM_ALLOCATED 62 | 63 | cdef int bp_decode_llr(self) 64 | 65 | cdef class bpgdg_decoder(bp_history_decoder): 66 | cdef BPGD* bpgd 67 | cdef BPGD_main_thread* bpgd_main_thread 68 | 69 | cdef double* llr_sum 70 | cdef int* cols 71 | cdef int new_n 72 | cdef char* bpgd_error 73 | cdef char** vn_stack 74 | cdef char** cn_stack 75 | cdef char** cn_degree_stack 76 | cdef char* decision_value_stack 77 | cdef int* decision_vn_stack 78 | cdef int* alt_depth_stack 79 | cdef double min_pm 80 | cdef bint multi_thread 81 | cdef bint low_error_mode 82 | cdef bint always_restart 83 | cdef int min_converge_depth 84 | 85 | cdef int max_step, max_iter_per_step 86 | cdef int max_tree_depth, max_tree_branch_step 87 | cdef int max_side_depth, max_side_branch_step 88 | cdef int max_guess, used_guess 89 | 90 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector) 91 | cdef void gdg(self) 92 | cdef void gdg_multi_thread(self) 93 | cdef int select_vn(self, side_branch, current_depth) 94 | 95 | cdef class bpgd_decoder(bp_history_decoder): 96 | cdef BPGD* bpgd 97 | cdef int max_step, max_iter_per_step 98 | cdef double* llr_sum 99 | cdef int* cols 100 | cdef int new_n 101 | cdef char* bpgd_error 102 | cdef int min_converge_depth 103 | cdef double min_pm 104 | 105 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector) 106 | cdef void gd(self) -------------------------------------------------------------------------------- /src/bp_guessing_decoder.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | import numpy as np 3 | from scipy.sparse import spmatrix 4 | 5 | cdef class bp_history_decoder: 6 | def __cinit__(self, parity_check_matrix, **kwargs): 7 | channel_probs = kwargs.get("channel_probs") 8 | max_iter = kwargs.get("max_iter", 50) # pre-processing BP iterations 9 | ms_scaling_factor = kwargs.get("ms_scaling_factor", 1.0) 10 | 11 | self.MEM_ALLOCATED = False 12 | if isinstance(parity_check_matrix, np.ndarray) or isinstance(parity_check_matrix, spmatrix): 13 | pass 14 | else: 15 | raise TypeError(f"The input matrix is of an invalid type. Please input a np.ndarray or scipy.sparse.spmatrix object, not {type(parity_check_matrix)}") 16 | 17 | self.m, self.n = parity_check_matrix.shape 18 | 19 | if channel_probs[0] != None: 20 | if len(channel_probs) != self.n: 21 | raise ValueError(f"The length of the channel probability vector must be eqaul to the block length n={self.n}.") 22 | 23 | self.max_iter = max_iter 24 | self.ms_scaling_factor = ms_scaling_factor 25 | self.bp_iteration = 0 26 | self.converge = False 27 | 28 | if isinstance(parity_check_matrix, np.ndarray): 29 | self.H = numpy2mod2sparse(parity_check_matrix) #parity check matrix in sparse form 30 | elif isinstance(parity_check_matrix, spmatrix): 31 | self.H = spmatrix2mod2sparse(parity_check_matrix) 32 | 33 | assert self.n == self.H.n_cols # validate number of bits in mod2sparse format 34 | assert self.m == self.H.n_rows # validate number of checks in mod2sparse format 35 | self.synd = calloc(self.m, sizeof(char)) # syndrome string 36 | self.bp_decoding_synd = calloc(self.m, sizeof(char)) # decoded syndrome string 37 | self.bp_decoding = calloc(self.n, sizeof(char)) # BP decoding 38 | self.channel_llr = calloc(self.n, sizeof(double)) # channel probs 39 | self.log_prob_ratios = calloc(self.n, sizeof(double*)) # log probability ratios history 40 | self.history_length = 4 # BP posterior LLR history length 41 | for vn in range(self.n): 42 | self.log_prob_ratios[vn] = calloc(self.history_length, sizeof(double)) 43 | 44 | # for path metric (PM) calculation 45 | if channel_probs[0] != None: 46 | for vn in range(self.n): self.channel_llr[vn] = log((1-channel_probs[vn]) / channel_probs[vn]) 47 | 48 | cdef int bp_decode_llr(self): # messages in log-likelihood ratio (LLR) form. CN update rule: min sum 49 | cdef mod2entry *e 50 | cdef int cn, vn, iteration, sgn 51 | cdef bint equal 52 | cdef double temp, alpha 53 | 54 | # initialisation 55 | for vn in range(self.n): 56 | e = mod2sparse_first_in_col(self.H, vn) 57 | llr = self.channel_llr[vn] 58 | while not mod2sparse_at_end(e): 59 | e.bit_to_check = llr 60 | e = mod2sparse_next_in_col(e) 61 | 62 | for iteration in range(self.max_iter): 63 | self.bp_iteration += 1 64 | # min-sum check to bit messages 65 | alpha = self.ms_scaling_factor 66 | for cn in range(self.m): 67 | # iterate through all the active neighboring VNs 68 | e = mod2sparse_first_in_row(self.H, cn) 69 | temp = 1e308 70 | 71 | if self.synd[cn] == 1: sgn = 1 72 | else: sgn = 0 73 | # first pass, find the min abs value of all incoming messages, determine sign 74 | while not mod2sparse_at_end(e): 75 | 76 | e.check_to_bit = temp # store min from the left most to itself (not inclusive) 77 | e.sgn = sgn 78 | 79 | # clipping 80 | if e.bit_to_check > 50.0: e.bit_to_check = 50.0 81 | elif e.bit_to_check < -50.0: e.bit_to_check = -50.0 82 | 83 | if abs(e.bit_to_check) < temp: 84 | temp = abs(e.bit_to_check) 85 | if e.bit_to_check <= 0: sgn = 1 - sgn 86 | e = mod2sparse_next_in_row(e) 87 | 88 | # second pass, set min to second min, others to min 89 | e = mod2sparse_last_in_row(self.H, cn) 90 | temp = 1e308 91 | sgn = 0 92 | while not mod2sparse_at_end(e): 93 | if temp < e.check_to_bit: 94 | e.check_to_bit = temp 95 | e.sgn += sgn 96 | 97 | e.check_to_bit *= ((-1)**e.sgn) * alpha 98 | 99 | if abs(e.bit_to_check) < temp: # store the min from the right most to itself 100 | temp = abs(e.bit_to_check) 101 | if e.bit_to_check <= 0: sgn = 1 - sgn 102 | 103 | e = mod2sparse_prev_in_row(e) 104 | 105 | # bit-to-check messages 106 | for vn in range(self.n): 107 | 108 | e = mod2sparse_first_in_col(self.H, vn) 109 | temp = self.channel_llr[vn] 110 | 111 | while not mod2sparse_at_end(e): 112 | 113 | e.bit_to_check = temp # sum from the left to itself 114 | temp += e.check_to_bit 115 | e = mod2sparse_next_in_col(e) 116 | 117 | self.log_prob_ratios[vn][iteration % self.history_length] = temp 118 | if temp <= 0: self.bp_decoding[vn] = 1 119 | else: self.bp_decoding[vn] = 0 120 | 121 | e = mod2sparse_last_in_col(self.H, vn) 122 | temp = 0.0 123 | while not mod2sparse_at_end(e): 124 | e.bit_to_check += temp # plus the sum from the right to itself 125 | temp += e.check_to_bit 126 | e = mod2sparse_prev_in_col(e) 127 | 128 | # check if converged 129 | mod2sparse_mulvec_cpp(self.H, self.bp_decoding, self.bp_decoding_synd) 130 | 131 | equal = True 132 | for cn in range(self.m): 133 | if self.synd[cn] != self.bp_decoding_synd[cn]: 134 | equal = False 135 | break 136 | if equal: 137 | return 1 138 | 139 | return 0 140 | 141 | 142 | def __dealloc__(self): 143 | 144 | if self.MEM_ALLOCATED: 145 | free(self.synd) 146 | free(self.bp_decoding_synd) 147 | free(self.bp_decoding) 148 | free(self.channel_llr) 149 | 150 | for i in range(self.n): 151 | free(self.log_prob_ratios[i]) 152 | free(self.log_prob_ratios) 153 | 154 | mod2sparse_free_cpp(self.H) 155 | 156 | @property 157 | def converge(self): 158 | return self.converge 159 | 160 | cdef class bpgdg_decoder(bp_history_decoder): 161 | def __cinit__(self, parith_check_matrix, **kwargs): 162 | max_iter_per_step = kwargs.get("max_iter_per_step", 6) 163 | max_step = kwargs.get("max_step", 25) 164 | max_tree_depth = kwargs.get("max_tree_depth", 3) 165 | max_side_depth = kwargs.get("max_side_depth", 10) 166 | max_tree_branch_step = kwargs.get("max_tree_branch_step", 10) 167 | max_side_branch_step = kwargs.get("max_side_branch_step", 10) 168 | gdg_factor = kwargs.get("gdg_factor", 1.0) 169 | new_n = kwargs.get("new_n", None) 170 | multi_thread = kwargs.get("multi_thread", False) 171 | low_error_mode = kwargs.get("low_error_mode", False) 172 | 173 | self.MEM_ALLOCATED = False 174 | 175 | self.max_iter_per_step = max_iter_per_step 176 | self.max_step = max_step 177 | self.max_tree_depth = max_tree_depth 178 | self.max_side_depth = max_side_depth 179 | self.max_tree_branch_step = max_tree_branch_step 180 | self.max_side_branch_step = max_side_branch_step 181 | self.max_guess = (2 ** max_tree_depth - 1) * 2 + max_side_depth - max_tree_depth 182 | self.used_guess = 0 183 | 184 | # things for post-processing 185 | self.llr_sum = calloc(self.n, sizeof(double)) 186 | self.cols = calloc(self.n, sizeof(int)) # for index sort according to BP posterior LLR 187 | if new_n is None: 188 | self.new_n = min(self.n, self.m * 2) 189 | else: 190 | self.new_n = min(new_n, self.n) 191 | self.bpgd_error = calloc(self.new_n, sizeof(char)) 192 | if multi_thread: 193 | self.bpgd_main_thread = new BPGD_main_thread(self.m, self.new_n, self.max_iter_per_step, self.max_step, 194 | self.max_tree_depth, self.max_side_depth, self.max_tree_branch_step, self.max_side_branch_step, low_error_mode, gdg_factor) 195 | else: 196 | # for saved guessing history 197 | # for VN: 0, 1 stands for decided value, -1 indicates undecided 198 | # for CN: 0, 1 stands for active checks, -1 indicates already cleared 199 | self.vn_stack = calloc(self.max_guess, sizeof(char*)) 200 | for i in range(self.max_guess): 201 | self.vn_stack[i] = calloc(self.new_n, sizeof(char)) 202 | # peeling wouldn't affect [alive] VN degree, thus no stack for vn_degree 203 | self.cn_stack = calloc(self.max_guess, sizeof(char*)) 204 | for i in range(self.max_guess): 205 | self.cn_stack[i] = calloc(self.m, sizeof(char)) 206 | self.cn_degree_stack = calloc(self.max_guess, sizeof(char*)) 207 | for i in range(self.max_guess): 208 | self.cn_degree_stack[i] = calloc(self.m, sizeof(char)) 209 | self.decision_value_stack = calloc(self.max_guess, sizeof(char)) 210 | self.decision_vn_stack = calloc(self.max_guess, sizeof(int)) 211 | self.alt_depth_stack = calloc(self.max_guess, sizeof(int)) 212 | self.bpgd = new BPGD(self.m, self.new_n, self.max_iter_per_step, low_error_mode, gdg_factor) 213 | 214 | self.MEM_ALLOCATED=True 215 | 216 | self.min_pm = 100000.0 # minimum BP path metric 217 | self.multi_thread = multi_thread 218 | self.low_error_mode = low_error_mode 219 | self.min_converge_depth = 100 220 | 221 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector): 222 | cdef int input_length = input_vector.shape[0] 223 | 224 | if input_length == self.m: 225 | self.synd = numpy2char(input_vector, self.synd) 226 | if self.bp_decode_llr(): 227 | self.converge = True 228 | return char2numpy(self.bp_decoding, self.n) 229 | if self.multi_thread: 230 | self.gdg_multi_thread() 231 | else: 232 | self.gdg() 233 | else: 234 | raise ValueError(f"The input to the ldpc.bp_decoder.decode must be a syndrome (of length={self.m}). The inputted vector has length={input_length}. Valid formats are `np.ndarray` or `scipy.sparse.spmatrix`.") 235 | 236 | return char2numpy(self.bp_decoding, self.n) 237 | 238 | cdef void gdg_multi_thread(self): 239 | cdef int vn 240 | for vn in range(self.n): 241 | history = self.log_prob_ratios[vn] 242 | self.llr_sum[vn] = history[0] + history[1] + history[2] + history[3] 243 | 244 | index_sort(self.llr_sum, self.cols, self.n) 245 | 246 | self.bpgd_main_thread.do_work(self.H, self.cols, self.channel_llr, self.synd) 247 | self.converge = (self.bpgd_main_thread.min_pm < 9999.0) 248 | for vn in range(self.new_n): 249 | self.bp_decoding[self.cols[vn]] = self.bpgd_main_thread.min_pm_error[vn] 250 | for vn in range(self.new_n, self.n): 251 | self.bp_decoding[self.cols[vn]] = 0 252 | 253 | 254 | cdef void gdg(self): 255 | # if BP doesn't converge, run GDG post-processing 256 | cdef int i, j, vn, cn, current_depth = 0, temp_converge 257 | cdef double pm = 0.0 258 | self.converge = False 259 | for vn in range(self.n): 260 | history = self.log_prob_ratios[vn] 261 | self.llr_sum[vn] = history[0] + history[1] + history[2] + history[3] 262 | 263 | index_sort(self.llr_sum, self.cols, self.n) 264 | 265 | # print("single thread sort", end=" ") 266 | # for i in range(100): 267 | # print(self.cols[i], end=" ") 268 | # print() 269 | 270 | for vn in range(self.new_n, self.n): 271 | self.bp_decoding[self.cols[vn]] = 0 272 | 273 | if self.bpgd.reset(self.H, self.cols, self.channel_llr, self.synd) == -1: 274 | return 275 | # guessing part reset 276 | self.min_pm = 10000.0 277 | self.used_guess = 0 278 | self.bp_iteration = 0 279 | self.min_converge_depth = self.max_step 280 | # do GDG 281 | # phase 1: growth of main branch and side branches 282 | for current_depth in range(self.max_step): 283 | temp_converge = self.bpgd.min_sum_log() 284 | if temp_converge: 285 | self.converge = True 286 | self.min_converge_depth = current_depth 287 | pm = self.bpgd.get_pm() 288 | for vn in range(self.new_n): 289 | self.bpgd_error[vn] = self.bpgd.error[vn] 290 | self.min_pm = pm 291 | # print("main branch converge with pm", pm) 292 | break 293 | if self.select_vn(side_branch=False, current_depth=current_depth) == -1: 294 | break 295 | if not self.converge: # still copy main branch decision 296 | # print("main branch did not converge") 297 | for vn in range(self.new_n): 298 | self.bpgd_error[vn] = self.bpgd.error[vn] 299 | # print("single thread ends at depth", self.min_converge_depth, "with pm", self.min_pm) 300 | # phase 2: try all the side branches, and select the one that converges and has min pm 301 | i = 0 302 | while i < self.used_guess: 303 | current_depth = self.alt_depth_stack[i] 304 | if current_depth > self.min_converge_depth: # not necessary to explore this path, pm won't be smaller heuristically 305 | i += 1 306 | continue 307 | # load stored snapshot, bp reinit 308 | self.bpgd.set_masks(self.vn_stack[i], self.cn_stack[i], self.cn_degree_stack[i]) 309 | # make decision and peel 310 | # print("single: side branch", i, "at depth", self.alt_depth_stack[i], "load vn", self.decision_vn_stack[i], "with value", self.decision_value_stack[i]); 311 | if self.bpgd.vn_set_value(self.decision_vn_stack[i], self.decision_value_stack[i]) == -1: 312 | i += 1 313 | continue 314 | if self.bpgd.peel() == -1: 315 | i += 1 316 | continue 317 | for j in range(self.max_side_branch_step): 318 | current_depth = self.alt_depth_stack[i] + j 319 | temp_converge = self.bpgd.min_sum_log() 320 | if temp_converge: 321 | self.converge = True 322 | pm = self.bpgd.get_pm() 323 | # print("single: side branch", i, "converge with pm", pm) 324 | if pm < self.min_pm: 325 | if current_depth < self.min_converge_depth: 326 | self.min_converge_depth = current_depth 327 | for vn in range(self.new_n): 328 | self.bpgd_error[vn] = self.bpgd.error[vn] 329 | self.min_pm = pm 330 | break 331 | if current_depth > self.min_converge_depth + 2: # heuristic early stop 332 | break 333 | if self.select_vn(side_branch=True, current_depth=current_depth) == -1: 334 | break 335 | i += 1 336 | 337 | for vn in range(self.new_n): 338 | self.bp_decoding[self.cols[vn]] = self.bpgd_error[vn] 339 | 340 | cdef int select_vn(self, side_branch, current_depth): 341 | cdef double A = -3.0 if not side_branch else 0.0 342 | cdef double A_sum = -12.0 if not side_branch else -10.0 343 | if current_depth == 0: A_sum = -16.0 # TODO: try if this is critical 344 | cdef double C = 30.0 345 | cdef double D = 3.0 346 | cdef bint all_smaller_than_A, all_negative, all_larger_than_C, all_larger_than_D 347 | cdef int vn, cn, i, sum_smallest_vn = -1, sum_smallest_all_neg_vn = -1 348 | cdef int vn_degree 349 | cdef int num_flip 350 | cdef mod2entry* e 351 | cdef int guess_vn = -1 352 | cdef char favor = 1, unfavor = 0 353 | cdef double* history 354 | cdef double llr, sum_smallest = 10000, history_sum = 0.0, sum_smallest_all_neg = 10000 355 | cdef bint guess = True 356 | 357 | for vn in range(self.new_n): 358 | if self.bpgd.vn_mask[vn] != -1: # skip inactive vn 359 | continue 360 | if self.bpgd.vn_degree[vn] <= 2: # skip degree 1 or 2 vn 361 | continue 362 | # how many syndrome inconsistency can flipping this vn reduce (#unsatisfied CN neighbor) 363 | num_flip = 0 364 | e = mod2sparse_first_in_col(self.bpgd.pcm, vn) 365 | while not mod2sparse_at_end(e): 366 | if self.bpgd.cn_mask[e.row] == -1: 367 | e = mod2sparse_next_in_col(e) 368 | continue 369 | cn = e.row 370 | if self.bpgd.syndrome[cn] != self.bpgd.temp_syndrome[cn]: 371 | num_flip += 1 372 | e = mod2sparse_next_in_col(e) 373 | 374 | history = self.bpgd.llr_posterior[vn] 375 | all_smaller_than_A = True 376 | all_negative = True 377 | all_larger_than_C = True 378 | all_larger_than_D = True 379 | history_sum = 0.0 380 | for i in range(self.history_length): 381 | llr = history[i] 382 | history_sum += llr 383 | if llr < C: all_larger_than_C = False 384 | if llr < D: all_larger_than_D = False 385 | if llr > A: all_smaller_than_A = False 386 | if llr > 0.0: all_negative = False 387 | if (not self.low_error_mode) and all_larger_than_C and current_depth < 4: # use current_depth instead of num_active_vn 388 | if self.bpgd.vn_set_value(vn, 0) == -1: 389 | return -1 390 | elif (not self.low_error_mode) and num_flip >= 3 and all_larger_than_D: 391 | if self.bpgd.vn_set_value(vn, 0) == -1: 392 | return -1 393 | elif (not self.low_error_mode) and (all_smaller_than_A and history_sum < A_sum): 394 | if self.bpgd.vn_set_value(vn, 1) == -1: 395 | return -1 396 | else: 397 | if history_sum < sum_smallest: 398 | sum_smallest = history_sum 399 | sum_smallest_vn = vn 400 | if all_negative and history_sum < sum_smallest_all_neg: 401 | sum_smallest_all_neg = history_sum 402 | sum_smallest_all_neg_vn = vn 403 | 404 | if self.bpgd.peel() == -1: # aggressive decimation failed 405 | return -1 406 | 407 | if sum_smallest_all_neg_vn != -1: 408 | guess_vn = sum_smallest_all_neg_vn 409 | favor = 1 410 | else: 411 | guess_vn = sum_smallest_vn 412 | favor = 0 if sum_smallest > 0 else 1 413 | 414 | unfavor = 1 - favor 415 | 416 | if current_depth > self.min_converge_depth: # side branch 417 | guess = False 418 | if (not side_branch) and current_depth >= self.max_side_depth: 419 | guess = False 420 | if side_branch and current_depth > self.max_tree_depth: 421 | guess = False 422 | if guess and (self.used_guess < self.max_guess): 423 | used_guess = self.used_guess 424 | self.decision_value_stack[used_guess] = unfavor 425 | self.decision_vn_stack[used_guess] = guess_vn 426 | self.alt_depth_stack[used_guess] = current_depth + 1 427 | for vn in range(self.new_n): 428 | self.vn_stack[used_guess][vn] = self.bpgd.vn_mask[vn] 429 | for cn in range(self.m): 430 | self.cn_stack[used_guess][cn] = self.bpgd.cn_mask[cn] 431 | for cn in range(self.m): 432 | self.cn_degree_stack[used_guess][cn] = self.bpgd.cn_degree[cn] 433 | self.used_guess = used_guess + 1 434 | 435 | # print("decide on guess vn", guess_vn, "original", self.cols[guess_vn], "degree", self.bpgd.vn_degree[guess_vn], "value", favor) 436 | # history = self.bpgd.llr_posterior[guess_vn] 437 | # print("history", history[0], history[1], history[2], history[3]) 438 | if self.bpgd.vn_set_value(guess_vn, favor) == -1: 439 | return -1 440 | if self.bpgd.peel() == -1: 441 | return -1 442 | return 0 443 | 444 | def __dealloc__(self): 445 | 446 | if self.MEM_ALLOCATED: 447 | free(self.bpgd_error) 448 | free(self.llr_sum) 449 | free(self.cols) 450 | 451 | if self.multi_thread: 452 | del self.bpgd_main_thread 453 | else: 454 | for i in range(self.max_guess): 455 | free(self.vn_stack[i]) 456 | free(self.vn_stack) 457 | 458 | for i in range(self.max_guess): 459 | free(self.cn_stack[i]) 460 | free(self.cn_stack) 461 | 462 | for i in range(self.max_guess): 463 | free(self.cn_degree_stack[i]) 464 | free(self.cn_degree_stack) 465 | 466 | free(self.decision_value_stack) 467 | free(self.decision_vn_stack) 468 | free(self.alt_depth_stack) 469 | del self.bpgd 470 | 471 | 472 | 473 | cdef class bpgd_decoder(bp_history_decoder): 474 | def __cinit__(self, parity_check_matrix, **kwargs): 475 | max_iter_per_step = kwargs.get("max_iter_per_step", 6) 476 | max_step = kwargs.get("max_step", 25) 477 | gd_factor = kwargs.get("gd_factor", 1.0) 478 | new_n = kwargs.get("new_n", None) 479 | 480 | # print("BPGD R =", max_step, "T =", max_iter_per_step) 481 | self.MEM_ALLOCATED = False 482 | 483 | self.max_iter_per_step = max_iter_per_step 484 | self.max_step = max_step 485 | self.min_pm = 10000.0 486 | 487 | self.llr_sum = calloc(self.n, sizeof(double)) 488 | # things for post-processing 489 | self.cols = calloc(self.n, sizeof(int)) # for index sort according to BP posterior LLR 490 | if new_n is None: 491 | self.new_n = min(self.n, self.m * 2) 492 | else: 493 | self.new_n = min(new_n, self.n) 494 | self.bpgd_error = calloc(self.new_n, sizeof(char)) 495 | self.bpgd = new BPGD(self.m, self.new_n, self.max_iter_per_step, False, gd_factor) 496 | 497 | self.MEM_ALLOCATED=True 498 | 499 | self.min_converge_depth = 100 500 | 501 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector): 502 | cdef int input_length = input_vector.shape[0] 503 | 504 | if input_length == self.m: 505 | self.synd = numpy2char(input_vector, self.synd) 506 | if self.max_iter > -1: 507 | if self.bp_decode_llr(): 508 | self.converge = True 509 | return char2numpy(self.bp_decoding, self.n) 510 | self.gd() 511 | else: 512 | raise ValueError(f"The input to the ldpc.bp_decoder.decode must be a syndrome (of length={self.m}). The inputted vector has length={input_length}. Valid formats are `np.ndarray` or `scipy.sparse.spmatrix`.") 513 | 514 | return char2numpy(self.bp_decoding, self.n) 515 | 516 | 517 | cdef void gd(self): 518 | # if BP doesn't converge, run GDG post-processing 519 | cdef int i, j, vn, cn, current_depth = 0, temp_converge 520 | cdef double pm = 0.0 521 | self.converge = False 522 | for vn in range(self.n): 523 | history = self.log_prob_ratios[vn] 524 | self.llr_sum[vn] = history[0] + history[1] + history[2] + history[3] 525 | 526 | index_sort(self.llr_sum, self.cols, self.n) 527 | 528 | for vn in range(self.new_n, self.n): 529 | self.bp_decoding[self.cols[vn]] = 0 530 | 531 | if self.bpgd.reset(self.H, self.cols, self.channel_llr, self.synd) == -1: 532 | print("bpgd reset fail") 533 | return 534 | # GD reset 535 | self.min_pm = 10000.0 536 | self.bp_iteration = 0 537 | self.min_converge_depth = self.max_step 538 | # do GD 539 | # print("GD begins") 540 | for current_depth in range(self.max_step): 541 | temp_converge = self.bpgd.min_sum_log() 542 | # print("rest vn", self.bpgd.num_active_vn) 543 | if temp_converge: 544 | self.converge = True 545 | self.min_converge_depth = current_depth 546 | pm = self.bpgd.get_pm() 547 | for vn in range(self.new_n): 548 | self.bpgd_error[vn] = self.bpgd.error[vn] 549 | self.min_pm = pm 550 | # print("converge with pm", pm) 551 | break 552 | if self.bpgd.decimate_vn_reliable(current_depth, 1.0) == -1: 553 | break 554 | if not self.converge: # still copy decision 555 | # print("did not converge") 556 | for vn in range(self.new_n): 557 | self.bpgd_error[vn] = self.bpgd.error[vn] 558 | # print("min converge depth", self.min_converge_depth) 559 | for vn in range(self.new_n): 560 | self.bp_decoding[self.cols[vn]] = self.bpgd_error[vn] 561 | 562 | 563 | def __dealloc__(self): 564 | 565 | if self.MEM_ALLOCATED: 566 | free(self.bpgd_error) 567 | free(self.llr_sum) 568 | free(self.cols) 569 | 570 | del self.bpgd 571 | -------------------------------------------------------------------------------- /src/build_SHYPS_circuit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools 3 | import stim 4 | import matplotlib.pyplot as plt 5 | from functools import reduce 6 | from .codes_q import gcd, poly_divmod, coeff2poly 7 | from .utils import inverse, edge_coloring_bipartite 8 | 9 | def build_SHYPS_circuit(r, p, num_repeat, z_basis=True, use_both=False): 10 | n_r = 2**r - 1 11 | # Primitive polynomial h(x)=1+x^a+x^b\in\mathbb{F}_2[x]/(x^{n_r}-1) 12 | # such that gcd(h(x),x^{n_r}-1) is a primitive polynomial of degree r 13 | if r == 3: 14 | primitive_poly = [0,2,3] # h(x)=x^0+x^2+x^3 15 | elif r == 4: 16 | primitive_poly = [0,3,4] # h(x)=x^0+x^3+x^4 17 | elif r == 5: 18 | primitive_poly = [0,2,5] # h(x)=x^0+x^2+x^5 19 | else: 20 | print(f"Unsupported r={r}, please find primitive polynomial yourself") 21 | assert gcd([0,n_r], primitive_poly) == primitive_poly # check h(x) indeed divides (x^{n_r}-1) 22 | primitive_poly = coeff2poly(primitive_poly)[::-1] # list of coeff, in increasing order of degree 23 | # Define overcomplete PCM for classical simplex code 24 | H_first_row = np.zeros(n_r, dtype=int) 25 | H_first_row[:len(primitive_poly)] = primitive_poly 26 | H = np.array([np.roll(H_first_row, i) for i in range(n_r)]) # shape n_r by n_r 27 | print(H) 28 | generator_poly, _ = poly_divmod(coeff2poly([0,n_r])[::-1], primitive_poly, 2) # g(x) = (x^{n_r}-1) / h(x) 29 | G_first_row = np.zeros(n_r, dtype=int) 30 | G_first_row[:len(generator_poly)] = generator_poly 31 | G = np.array([np.roll(G_first_row, i) for i in range(r)]) # shape r by n_r 32 | print(G) 33 | assert not np.any(G @ H % 2) # GH=0, HG=0 34 | 35 | identity = np.identity(n_r, dtype=int) 36 | S_X = np.kron(H.T, G) # X stabilizers 37 | gauge_X = np.kron(H.T, identity) # X gauge operators 38 | aggregate_X = np.kron(identity, G) 39 | # to aggregate gauge operators into stabilizer 40 | # S_X = H \otimes G = (I \otimes G) gauge_X 41 | S_Z = np.kron(G, H.T) # Z stabilizers 42 | assert np.array_equal(S_Z.T, np.kron(G.T, H)) # (A \otimes B)^T = A^T \otimes B^T 43 | gauge_Z = np.kron(identity, H.T) # Z gauge operators 44 | aggregate_Z = np.kron(G, identity) 45 | 46 | assert not np.any(S_X @ S_Z.T % 2) # X and Z stabilizers commute 47 | assert not np.any(gauge_X @ S_Z.T % 2) # gauge X operators commute with Z stabilizers 48 | assert not np.any(S_X @ gauge_Z.T % 2) # gauge Z operators commute with X stabilizers 49 | 50 | # to define logical operators, first get the pivot matrix P (shape r by n_r) 51 | # such that P G^T = I 52 | P = inverse(G.T) 53 | # print(P) 54 | L_X = np.kron(P, G) # X logicals 55 | L_Z = np.kron(G, P) # Z logicals 56 | assert not np.any(gauge_X @ L_Z.T % 2) # gauge X operators commute with Z logicals 57 | assert not np.any(L_X @ gauge_Z.T % 2) # gauge Z operators commute with X logicals 58 | 59 | N = n_r ** 2 # number of data qubits, also number of X and Z gauge operators 60 | 61 | color_dict_gauge_X, num_colors_X = edge_coloring_bipartite(gauge_X) 62 | color_dict_gauge_Z, num_colors_Z = edge_coloring_bipartite(gauge_Z) 63 | assert num_colors_X == 3 64 | assert num_colors_Z == 3 65 | 66 | for color in range(3): 67 | print(f"color={color}, #edges: {len(color_dict_gauge_Z[color])}") 68 | X_gauge_offset = 0 69 | data_offset = N # there are N X gauge operators 70 | Z_gauge_offset = 2*N 71 | 72 | # first round (encoding round) detector circuit, only put on one basis, no previous round to XOR with 73 | detector_circuit_str = "" 74 | for row in (aggregate_Z if z_basis else aggregate_X): 75 | temp = "DETECTOR " 76 | for i in row.nonzero()[0]: 77 | temp += f"rec[{-N+i}] " 78 | detector_circuit_str += f"{temp}\n" 79 | detector_circuit = stim.Circuit(detector_circuit_str) 80 | 81 | X_detector_circuit_str = "" 82 | for row in aggregate_X: 83 | temp = "DETECTOR " 84 | for i in row.nonzero()[0]: 85 | temp += f"rec[{-N+i}] rec[{-3*N+i}] " 86 | X_detector_circuit_str += f"{temp}\n" 87 | X_detector_circuit = stim.Circuit(X_detector_circuit_str) 88 | 89 | Z_detector_circuit_str = "" 90 | for row in aggregate_Z: 91 | temp = "DETECTOR " 92 | for i in row.nonzero()[0]: 93 | temp += f"rec[{-N+i}] rec[{-3*N+i}] " 94 | Z_detector_circuit_str += f"{temp}\n" 95 | Z_detector_circuit = stim.Circuit(Z_detector_circuit_str) 96 | 97 | def append_block(circuit, repeat=False): 98 | if repeat: # not encoding round 99 | for i in range(N): 100 | circuit.append("X_ERROR", Z_gauge_offset + i, p) 101 | circuit.append("Z_ERROR", X_gauge_offset + i, p) 102 | circuit.append("DEPOLARIZE1", data_offset + i, p) 103 | circuit.append("TICK") 104 | 105 | for color in range(num_colors_Z): 106 | for Z_gauge_idx, data_idx in color_dict_gauge_Z[color]: 107 | circuit.append("CNOT", [data_offset + data_idx, Z_gauge_offset + Z_gauge_idx]) 108 | circuit.append("DEPOLARIZE2", [data_offset + data_idx, Z_gauge_offset + Z_gauge_idx], p) 109 | circuit.append("TICK") 110 | 111 | # measure Z gauge operators 112 | for i in range(N): 113 | circuit.append("X_ERROR", Z_gauge_offset + i, p) 114 | circuit.append("M", Z_gauge_offset + i) 115 | if z_basis: 116 | circuit += (Z_detector_circuit if repeat else detector_circuit) 117 | # initialize X gauge operators 118 | for i in range(N): 119 | circuit.append("RX", X_gauge_offset + i) 120 | circuit.append("Z_ERROR", X_gauge_offset + i, p) 121 | circuit.append("TICK") 122 | 123 | for color in range(num_colors_X): 124 | for X_gauge_idx, data_idx in color_dict_gauge_X[color]: 125 | # for data_idx, X_gauge_idx in color_dict_gauge_X[color]: 126 | circuit.append("CNOT", [X_gauge_offset + X_gauge_idx, data_offset + data_idx]) 127 | circuit.append("DEPOLARIZE2", [X_gauge_offset + X_gauge_idx, data_offset + data_idx], p) 128 | 129 | circuit.append("TICK") 130 | 131 | # measure X gauge operators 132 | for i in range(N): 133 | circuit.append("Z_ERROR", X_gauge_offset + i, p) 134 | circuit.append("MX", X_gauge_offset + i) 135 | if not z_basis: 136 | circuit += (X_detector_circuit if repeat else detector_circuit) 137 | # initialize Z gauge operators 138 | for i in range(N): 139 | circuit.append("R", Z_gauge_offset + i) 140 | circuit.append("X_ERROR", Z_gauge_offset + i, p) 141 | circuit.append("TICK") 142 | 143 | 144 | 145 | circuit = stim.Circuit() 146 | for i in range(N): 147 | circuit.append("RX", X_gauge_offset + i) 148 | circuit.append("Z_ERROR", X_gauge_offset + i, p) 149 | circuit.append("R", Z_gauge_offset + i) 150 | circuit.append("X_ERROR", Z_gauge_offset + i, p) 151 | for i in range(N): 152 | circuit.append("R" if z_basis else "RX", data_offset + i) 153 | circuit.append("X_ERROR" if z_basis else "Z_ERROR", data_offset + i, p) 154 | 155 | # begin round tick 156 | circuit.append("TICK") 157 | append_block(circuit, repeat=False) # encoding round 158 | 159 | rep_circuit = stim.Circuit() 160 | append_block(rep_circuit, repeat=True) 161 | circuit += (num_repeat-1) * rep_circuit 162 | 163 | for i in range(N): 164 | circuit.append("X_ERROR" if z_basis else "Z_ERROR", data_offset + i, p) 165 | circuit.append("M" if z_basis else "MX", data_offset + i) 166 | 167 | pcm = S_Z if z_basis else S_X 168 | aggregate_matrix = aggregate_Z if z_basis else aggregate_X 169 | logical_pcm = L_Z if z_basis else L_X 170 | stab_detector_circuit_str = "" 171 | row_idx = 0 172 | for row in pcm: 173 | det_str = "DETECTOR " 174 | for data_idx in row.nonzero()[0]: 175 | det_str += f"rec[{-N+data_idx}] " 176 | for gauge_idx in aggregate_matrix[row_idx].nonzero()[0]: 177 | det_str += f"rec[{-(3 if z_basis else 2)*N+gauge_idx}] " 178 | stab_detector_circuit_str += f"{det_str}\n" 179 | row_idx += 1 180 | circuit += stim.Circuit(stab_detector_circuit_str) 181 | 182 | log_detector_circuit_str = "" # logical operators 183 | row_idx = 0 184 | for row in logical_pcm: 185 | obs_str = f"OBSERVABLE_INCLUDE({row_idx}) " 186 | for data_idx in row.nonzero()[0]: 187 | obs_str += f"rec[{-N+data_idx}] " 188 | log_detector_circuit_str += f"{obs_str}\n" 189 | row_idx += 1 190 | circuit += stim.Circuit(log_detector_circuit_str) 191 | 192 | return circuit -------------------------------------------------------------------------------- /src/build_circuit.py: -------------------------------------------------------------------------------- 1 | import stim 2 | import numpy as np 3 | from scipy.sparse import csc_matrix 4 | from typing import List, FrozenSet, Dict 5 | 6 | def build_circuit(code, A_list, B_list, p, num_repeat, z_basis=True, use_both=False, HZH=False): 7 | 8 | n = code.N 9 | a1, a2, a3 = A_list 10 | b1, b2, b3 = B_list 11 | 12 | def nnz(m): 13 | a, b = m.nonzero() 14 | return b[np.argsort(a)] 15 | 16 | A1, A2, A3 = nnz(a1), nnz(a2), nnz(a3) 17 | B1, B2, B3 = nnz(b1), nnz(b2), nnz(b3) 18 | 19 | A1_T, A2_T, A3_T = nnz(a1.T), nnz(a2.T), nnz(a3.T) 20 | B1_T, B2_T, B3_T = nnz(b1.T), nnz(b2.T), nnz(b3.T) 21 | 22 | # |+> ancilla: 0 ~ n/2-1. Control in CNOTs. 23 | X_check_offset = 0 24 | # L data qubits: n/2 ~ n-1. 25 | L_data_offset = n//2 26 | # R data qubits: n ~ 3n/2-1. 27 | R_data_offset = n 28 | # |0> ancilla: 3n/2 ~ 2n-1. Target in CNOTs. 29 | Z_check_offset = 3*n//2 30 | 31 | p_after_clifford_depolarization = p 32 | p_after_reset_flip_probability = p 33 | p_before_measure_flip_probability = p 34 | p_before_round_data_depolarization = p 35 | 36 | detector_circuit_str = "" 37 | for i in range(n//2): 38 | detector_circuit_str += f"DETECTOR rec[{-n//2+i}]\n" 39 | detector_circuit = stim.Circuit(detector_circuit_str) 40 | 41 | detector_repeat_circuit_str = "" 42 | for i in range(n//2): 43 | detector_repeat_circuit_str += f"DETECTOR rec[{-n//2+i}] rec[{-n-n//2+i}]\n" 44 | detector_repeat_circuit = stim.Circuit(detector_repeat_circuit_str) 45 | 46 | def append_blocks(circuit, repeat=False): 47 | # Round 1 48 | if repeat: 49 | for i in range(n//2): 50 | # measurement preparation errors 51 | circuit.append("X_ERROR", Z_check_offset + i, p_after_reset_flip_probability) 52 | if HZH: 53 | circuit.append("X_ERROR", X_check_offset + i, p_after_reset_flip_probability) 54 | circuit.append("H", [X_check_offset + i]) 55 | circuit.append("DEPOLARIZE1", X_check_offset + i, p_after_clifford_depolarization) 56 | else: 57 | circuit.append("Z_ERROR", X_check_offset + i, p_after_reset_flip_probability) 58 | # identity gate on R data 59 | circuit.append("DEPOLARIZE1", R_data_offset + i, p_before_round_data_depolarization) 60 | else: 61 | for i in range(n//2): 62 | circuit.append("H", [X_check_offset + i]) 63 | if HZH: 64 | circuit.append("DEPOLARIZE1", X_check_offset + i, p_after_clifford_depolarization) 65 | 66 | for i in range(n//2): 67 | # CNOTs from R data to to Z-checks 68 | circuit.append("CNOT", [R_data_offset + A1_T[i], Z_check_offset + i]) 69 | circuit.append("DEPOLARIZE2", [R_data_offset + A1_T[i], Z_check_offset + i], p_after_clifford_depolarization) 70 | # identity gate on L data 71 | circuit.append("DEPOLARIZE1", L_data_offset + i, p_before_round_data_depolarization) 72 | 73 | # tick 74 | circuit.append("TICK") 75 | 76 | # Round 2 77 | for i in range(n//2): 78 | # CNOTs from X-checks to L data 79 | circuit.append("CNOT", [X_check_offset + i, L_data_offset + A2[i]]) 80 | circuit.append("DEPOLARIZE2", [X_check_offset + i, L_data_offset + A2[i]], p_after_clifford_depolarization) 81 | # CNOTs from R data to Z-checks 82 | circuit.append("CNOT", [R_data_offset + A3_T[i], Z_check_offset + i]) 83 | circuit.append("DEPOLARIZE2", [R_data_offset + A3_T[i], Z_check_offset + i], p_after_clifford_depolarization) 84 | 85 | # tick 86 | circuit.append("TICK") 87 | 88 | # Round 3 89 | for i in range(n//2): 90 | # CNOTs from X-checks to R data 91 | circuit.append("CNOT", [X_check_offset + i, R_data_offset + B2[i]]) 92 | circuit.append("DEPOLARIZE2", [X_check_offset + i, R_data_offset + B2[i]], p_after_clifford_depolarization) 93 | # CNOTs from L data to Z-checks 94 | circuit.append("CNOT", [L_data_offset + B1_T[i], Z_check_offset + i]) 95 | circuit.append("DEPOLARIZE2", [L_data_offset + B1_T[i], Z_check_offset + i], p_after_clifford_depolarization) 96 | 97 | # tick 98 | circuit.append("TICK") 99 | 100 | # Round 4 101 | for i in range(n//2): 102 | # CNOTs from X-checks to R data 103 | circuit.append("CNOT", [X_check_offset + i, R_data_offset + B1[i]]) 104 | circuit.append("DEPOLARIZE2", [X_check_offset + i, R_data_offset + B1[i]], p_after_clifford_depolarization) 105 | # CNOTs from L data to Z-checks 106 | circuit.append("CNOT", [L_data_offset + B2_T[i], Z_check_offset + i]) 107 | circuit.append("DEPOLARIZE2", [L_data_offset + B2_T[i], Z_check_offset + i], p_after_clifford_depolarization) 108 | 109 | # tick 110 | circuit.append("TICK") 111 | 112 | # Round 5 113 | for i in range(n//2): 114 | # CNOTs from X-checks to R data 115 | circuit.append("CNOT", [X_check_offset + i, R_data_offset + B3[i]]) 116 | circuit.append("DEPOLARIZE2", [X_check_offset + i, R_data_offset + B3[i]], p_after_clifford_depolarization) 117 | # CNOTs from L data to Z-checks 118 | circuit.append("CNOT", [L_data_offset + B3_T[i], Z_check_offset + i]) 119 | circuit.append("DEPOLARIZE2", [L_data_offset + B3_T[i], Z_check_offset + i], p_after_clifford_depolarization) 120 | 121 | # tick 122 | circuit.append("TICK") 123 | 124 | # Round 6 125 | for i in range(n//2): 126 | # CNOTs from X-checks to L data 127 | circuit.append("CNOT", [X_check_offset + i, L_data_offset + A1[i]]) 128 | circuit.append("DEPOLARIZE2", [X_check_offset + i, L_data_offset + A1[i]], p_after_clifford_depolarization) 129 | # CNOTs from R data to Z-checks 130 | circuit.append("CNOT", [R_data_offset + A2_T[i], Z_check_offset + i]) 131 | circuit.append("DEPOLARIZE2", [R_data_offset + A2_T[i], Z_check_offset + i], p_after_clifford_depolarization) 132 | 133 | # tick 134 | circuit.append("TICK") 135 | 136 | # Round 7 137 | for i in range(n//2): 138 | # CNOTs from X-checks to L data 139 | circuit.append("CNOT", [X_check_offset + i, L_data_offset + A3[i]]) 140 | circuit.append("DEPOLARIZE2", [X_check_offset + i, L_data_offset + A3[i]], p_after_clifford_depolarization) 141 | # Measure Z-checks 142 | circuit.append("X_ERROR", Z_check_offset + i, p_before_measure_flip_probability) 143 | circuit.append("MR", [Z_check_offset + i]) 144 | # identity gates on R data, moved to beginning of the round 145 | # circuit.append("DEPOLARIZE1", R_data_offset + i, p_before_round_data_depolarization) 146 | 147 | # Z check detectors 148 | if z_basis: 149 | if repeat: 150 | circuit += detector_repeat_circuit 151 | else: 152 | circuit += detector_circuit 153 | elif use_both and repeat: 154 | circuit += detector_repeat_circuit 155 | 156 | # tick 157 | circuit.append("TICK") 158 | 159 | # Round 8 160 | for i in range(n//2): 161 | if HZH: 162 | circuit.append("H", [X_check_offset + i]) 163 | circuit.append("DEPOLARIZE1", X_check_offset + i, p_after_clifford_depolarization) 164 | circuit.append("X_ERROR", X_check_offset + i, p_before_measure_flip_probability) 165 | circuit.append("MR", [X_check_offset + i]) 166 | else: 167 | circuit.append("Z_ERROR", X_check_offset + i, p_before_measure_flip_probability) 168 | circuit.append("MRX", [X_check_offset + i]) 169 | # identity gates on L data, moved to beginning of the round 170 | # circuit.append("DEPOLARIZE1", L_data_offset + i, p_before_round_data_depolarization) 171 | 172 | # X basis detector 173 | if not z_basis: 174 | if repeat: 175 | circuit += detector_repeat_circuit 176 | else: 177 | circuit += detector_circuit 178 | elif use_both and repeat: 179 | circuit += detector_repeat_circuit 180 | 181 | # tick 182 | circuit.append("TICK") 183 | 184 | 185 | circuit = stim.Circuit() 186 | for i in range(n//2): # ancilla initialization 187 | circuit.append("R", X_check_offset + i) 188 | circuit.append("R", Z_check_offset + i) 189 | circuit.append("X_ERROR", X_check_offset + i, p_after_reset_flip_probability) 190 | circuit.append("X_ERROR", Z_check_offset + i, p_after_reset_flip_probability) 191 | for i in range(n): 192 | circuit.append("R" if z_basis else "RX", L_data_offset + i) 193 | circuit.append("X_ERROR" if z_basis else "Z_ERROR", L_data_offset + i, p_after_reset_flip_probability) 194 | 195 | # begin round tick 196 | circuit.append("TICK") 197 | append_blocks(circuit, repeat=False) # encoding round 198 | 199 | 200 | rep_circuit = stim.Circuit() 201 | append_blocks(rep_circuit, repeat=True) 202 | circuit += (num_repeat-1) * rep_circuit 203 | 204 | for i in range(0, n): 205 | # flip before collapsing data qubits 206 | # circuit.append("X_ERROR" if z_basis else "Z_ERROR", L_data_offset + i, p_before_measure_flip_probability) 207 | circuit.append("M" if z_basis else "MX", L_data_offset + i) 208 | 209 | pcm = code.hz if z_basis else code.hx 210 | logical_pcm = code.lz if z_basis else code.lx 211 | stab_detector_circuit_str = "" # stabilizers 212 | for i, s in enumerate(pcm): 213 | nnz = np.nonzero(s)[0] 214 | det_str = "DETECTOR" 215 | for ind in nnz: 216 | det_str += f" rec[{-n+ind}]" 217 | det_str += f" rec[{-n-n+i}]" if z_basis else f" rec[{-n-n//2+i}]" 218 | det_str += "\n" 219 | stab_detector_circuit_str += det_str 220 | stab_detector_circuit = stim.Circuit(stab_detector_circuit_str) 221 | circuit += stab_detector_circuit 222 | 223 | log_detector_circuit_str = "" # logical operators 224 | for i, l in enumerate(logical_pcm): 225 | nnz = np.nonzero(l)[0] 226 | det_str = f"OBSERVABLE_INCLUDE({i})" 227 | for ind in nnz: 228 | det_str += f" rec[{-n+ind}]" 229 | det_str += "\n" 230 | log_detector_circuit_str += det_str 231 | log_detector_circuit = stim.Circuit(log_detector_circuit_str) 232 | circuit += log_detector_circuit 233 | 234 | return circuit 235 | 236 | def dict_to_csc_matrix(elements_dict, shape): 237 | # Constructs a `scipy.sparse.csc_matrix` check matrix from a dictionary `elements_dict` 238 | # giving the indices of nonzero rows in each column. 239 | nnz = sum(len(v) for k, v in elements_dict.items()) 240 | data = np.ones(nnz, dtype=np.uint8) 241 | row_ind = np.zeros(nnz, dtype=np.int64) 242 | col_ind = np.zeros(nnz, dtype=np.int64) 243 | i = 0 244 | for col, v in elements_dict.items(): 245 | for row in v: 246 | row_ind[i] = row 247 | col_ind[i] = col 248 | i += 1 249 | return csc_matrix((data, (row_ind, col_ind)), shape=shape) 250 | 251 | def dem_to_check_matrices(dem: stim.DetectorErrorModel, return_col_dict=False): 252 | 253 | DL_ids: Dict[str, int] = {} # detectors + logical operators 254 | L_map: Dict[int, FrozenSet[int]] = {} # logical operators 255 | priors_dict: Dict[int, float] = {} # for each fault 256 | 257 | def handle_error(prob: float, detectors: List[int], observables: List[int]) -> None: 258 | dets = frozenset(detectors) 259 | obs = frozenset(observables) 260 | key = " ".join([f"D{s}" for s in sorted(dets)] + [f"L{s}" for s in sorted(obs)]) 261 | 262 | if key not in DL_ids: 263 | DL_ids[key] = len(DL_ids) 264 | priors_dict[DL_ids[key]] = 0.0 265 | 266 | hid = DL_ids[key] 267 | L_map[hid] = obs 268 | # priors_dict[hid] = priors_dict[hid] * (1 - prob) + prob * (1 - priors_dict[hid]) 269 | priors_dict[hid] += prob 270 | 271 | for instruction in dem.flattened(): 272 | if instruction.type == "error": 273 | dets: List[int] = [] 274 | frames: List[int] = [] 275 | t: stim.DemTarget 276 | p = instruction.args_copy()[0] 277 | for t in instruction.targets_copy(): 278 | if t.is_relative_detector_id(): 279 | dets.append(t.val) 280 | elif t.is_logical_observable_id(): 281 | frames.append(t.val) 282 | handle_error(p, dets, frames) 283 | elif instruction.type == "detector": 284 | pass 285 | elif instruction.type == "logical_observable": 286 | pass 287 | else: 288 | raise NotImplementedError() 289 | check_matrix = dict_to_csc_matrix({v: [int(s[1:]) for s in k.split(" ") if s.startswith("D")] 290 | for k, v in DL_ids.items()}, 291 | shape=(dem.num_detectors, len(DL_ids))) 292 | observables_matrix = dict_to_csc_matrix(L_map, shape=(dem.num_observables, len(DL_ids))) 293 | priors = np.zeros(len(DL_ids)) 294 | for i, p in priors_dict.items(): 295 | priors[i] = p 296 | 297 | if return_col_dict: 298 | return check_matrix, observables_matrix, priors, DL_ids 299 | return check_matrix, observables_matrix, priors 300 | -------------------------------------------------------------------------------- /src/c_util.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | from libc.stdlib cimport malloc, calloc,free 3 | import numpy as np 4 | cimport numpy as np 5 | cimport cython 6 | 7 | cdef char* numpy2char(np_array, char* char_array) 8 | cdef char* spmatrix2char(matrix, char* char_array) 9 | cdef int* numpy2int(np_array, int* int_array) 10 | cdef double* numpy2double(np.ndarray[np.float_t, ndim=1] np_array, double* double_array) 11 | cdef np.ndarray[np.int_t, ndim=1] char2numpy(char* char_array, int n) 12 | cdef np.ndarray[np.int_t, ndim=2] stackchar2numpy(char* arr1, char* arr2, int n) 13 | cdef np.ndarray[np.float_t, ndim=1] double2numpy(double* char_array, int n) -------------------------------------------------------------------------------- /src/c_util.pyx: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | 3 | import numpy as np 4 | from scipy.sparse import spmatrix 5 | 6 | cdef char* numpy2char(np_array, char* char_array): 7 | cdef int n = np_array.shape[0] 8 | for i in range(n): char_array[i] = np_array[i] 9 | return char_array 10 | 11 | cdef char* spmatrix2char(matrix, char* char_array): 12 | cdef int n = matrix.shape[1] 13 | for i in range(n): char_array[i] = 0 14 | for i, j in zip(*matrix.nonzero()): 15 | char_array[j] = 1 16 | return char_array 17 | 18 | cdef int* numpy2int(np_array, int* int_array): 19 | cdef int n = np_array.shape[0] 20 | for i in range(n): int_array[i] = np_array[i] 21 | return int_array 22 | 23 | cdef double* numpy2double(np.ndarray[np.float_t, ndim=1] np_array, double* double_array): 24 | cdef int n = np_array.shape[0] 25 | for i in range(n): double_array[i] = np_array[i] 26 | return double_array 27 | 28 | cdef np.ndarray[np.int_t, ndim=1] char2numpy(char* char_array, int n): 29 | cdef np.ndarray[np.int_t, ndim=1] np_array = np.zeros(n).astype(int) 30 | for i in range(n): np_array[i] = char_array[i] 31 | return np_array 32 | 33 | cdef np.ndarray[np.float_t, ndim=1] double2numpy(double* char_array, int n): 34 | cdef np.ndarray[np.float_t, ndim=1] np_array = np.zeros(n) 35 | for i in range(n): np_array[i] = char_array[i] 36 | return np_array 37 | 38 | cdef np.ndarray[np.int_t, ndim=2] stackchar2numpy(char* arr1, char* arr2, int n): 39 | cdef np.ndarray[np.int_t, ndim=2] np_array = np.zeros((2, n)).astype(int) 40 | for i in range(n): 41 | np_array[0,i] = arr1[i] 42 | np_array[1,i] = arr2[i] 43 | return np_array -------------------------------------------------------------------------------- /src/codes_q.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from functools import reduce 3 | from scipy.sparse import identity, hstack, kron, csr_matrix 4 | from .utils import row_echelon, rank, kernel, compute_code_distance, inverse, int2bin 5 | from collections import deque 6 | 7 | class css_code(): # a refactored version of Roffe's package 8 | # do as less row echelon form calculation as possible. 9 | def __init__(self, hx=np.array([[]]), hz=np.array([[]]), code_distance=np.nan, name=None, name_prefix="", check_css=False): 10 | 11 | self.hx = hx # hx pcm 12 | self.hz = hz # hz pcm 13 | 14 | self.lx = np.array([[]]) # x logicals 15 | self.lz = np.array([[]]) # z logicals 16 | 17 | self.N = np.nan # block length 18 | self.K = np.nan # code dimension 19 | self.D = code_distance # do not take this as the real code distance 20 | # TODO: use QDistRnd to get the distance 21 | # the quantum code distance is the minimum weight of all the affine codes 22 | # each of which is a coset code of a non-trivial logical op + stabilizers 23 | self.L = np.nan # max column weight 24 | self.Q = np.nan # max row weight 25 | 26 | _, nx = self.hx.shape 27 | _, nz = self.hz.shape 28 | 29 | assert nx == nz, "hx and hz should have equal number of columns!" 30 | assert nx != 0, "number of variable nodes should not be zero!" 31 | if check_css: # For performance reason, default to False 32 | assert not np.any(hx @ hz.T % 2), "CSS constraint not satisfied" 33 | 34 | self.N = nx 35 | self.hx_perp, self.rank_hx, self.pivot_hx = kernel(hx) # orthogonal complement 36 | self.hz_perp, self.rank_hz, self.pivot_hz = kernel(hz) 37 | self.hx_basis = self.hx[self.pivot_hx] # same as calling row_basis(self.hx) 38 | self.hz_basis = self.hz[self.pivot_hz] # but saves one row echelon calculation 39 | self.K = self.N - self.rank_hx - self.rank_hz 40 | 41 | self.compute_ldpc_params() 42 | self.compute_logicals() 43 | if code_distance is np.nan: 44 | dx = compute_code_distance(self.hx_perp, is_pcm=False, is_basis=True) 45 | dz = compute_code_distance(self.hz_perp, is_pcm=False, is_basis=True) 46 | self.D = np.min([dx,dz]) # this is the distance of stabilizers, not the distance of the code 47 | 48 | self.name = f"{name_prefix}_n{self.N}_k{self.K}" if name is None else name 49 | 50 | def compute_ldpc_params(self): 51 | 52 | #column weights 53 | hx_l = np.max(np.sum(self.hx, axis=0)) 54 | hz_l = np.max(np.sum(self.hz, axis=0)) 55 | self.L = np.max([hx_l, hz_l]).astype(int) 56 | 57 | #row weights 58 | hx_q = np.max(np.sum(self.hx, axis=1)) 59 | hz_q = np.max(np.sum(self.hz, axis=1)) 60 | self.Q = np.max([hx_q, hz_q]).astype(int) 61 | 62 | def compute_logicals(self): 63 | 64 | def compute_lz(ker_hx, im_hzT): 65 | # lz logical operators 66 | # lz\in ker{hx} AND \notin Im(hz.T) 67 | # in the below we row reduce to find vectors in kx that are not in the image of hz.T. 68 | log_stack = np.vstack([im_hzT, ker_hx]) 69 | pivots = row_echelon(log_stack.T)[3] 70 | log_op_indices = [i for i in range(im_hzT.shape[0], log_stack.shape[0]) if i in pivots] 71 | log_ops = log_stack[log_op_indices] 72 | return log_ops 73 | 74 | self.lx = compute_lz(self.hz_perp, self.hx_basis) 75 | self.lz = compute_lz(self.hx_perp, self.hz_basis) 76 | 77 | return self.lx, self.lz 78 | 79 | def canonical_logicals(self): 80 | temp = inverse(self.lx @ self.lz.T % 2) 81 | self.lx = temp @ self.lx % 2 82 | 83 | def create_circulant_matrix(l, pows): 84 | h = np.zeros((l,l), dtype=int) 85 | for i in range(l): 86 | for c in pows: 87 | h[(i+c)%l, i] = 1 88 | return h 89 | 90 | 91 | def create_generalized_bicycle_codes(l, a, b, name=None): 92 | A = create_circulant_matrix(l, a) 93 | B = create_circulant_matrix(l, b) 94 | hx = np.hstack((A, B)) 95 | hz = np.hstack((B.T, A.T)) 96 | return css_code(hx, hz, name=name, name_prefix="GB") 97 | 98 | 99 | def hypergraph_product(h1, h2, name=None): 100 | m1, n1 = np.shape(h1) 101 | r1 = rank(h1) 102 | k1 = n1 - r1 103 | k1t = m1 - r1 104 | 105 | m2, n2 = np.shape(h2) 106 | r2 = rank(h2) 107 | k2 = n2 - r2 108 | k2t = m2 - r2 109 | 110 | #hgp code params 111 | N = n1 * n2 + m1 * m2 112 | K = k1 * k2 + k1t * k2t #number of logical qubits in hgp code 113 | 114 | #construct hx and hz 115 | h1 = csr_matrix(h1) 116 | hx1 = kron(h1, identity(n2, dtype=int)) 117 | hx2 = kron(identity(m1, dtype=int), h2.T) 118 | hx = hstack([hx1, hx2]).toarray() 119 | 120 | h2 = csr_matrix(h2) 121 | hz1 = kron(identity(n1, dtype=int), h2) 122 | hz2 = kron(h1.T, identity(m2, dtype=int)) 123 | hz = hstack([hz1, hz2]).toarray() 124 | return css_code(hx, hz, name=name, name_prefix="HP") 125 | 126 | def hamming_code(rank): 127 | rank = int(rank) 128 | num_rows = (2**rank) - 1 129 | pcm = np.zeros((num_rows, rank), dtype=int) 130 | for i in range(0, num_rows): 131 | pcm[i] = int2bin(i+1, rank) 132 | return pcm.T 133 | 134 | def rep_code(d): 135 | pcm = np.zeros((d-1, d), dtype=int) 136 | for i in range(d-1): 137 | pcm[i, i] = 1 138 | pcm[i, i+1] = 1 139 | return pcm 140 | 141 | def create_surface_codes(n): 142 | # [n^2+(n-1)^2, 1, n] surface code 143 | h = rep_code(n) 144 | return hypergraph_product(h, h, f"Surface_n{n**2 + (n-1)**2}_k{1}_d{n}") 145 | 146 | def set_pcm_row(n, pcm, row_idx, i, j): 147 | i1, j1 = (i+1) % n, (j+1) % n 148 | pcm[row_idx][i*n+j] = pcm[row_idx][i1*n+j1] = 1 149 | pcm[row_idx][i1*n+j] = pcm[row_idx][i*n+j1] = 1 150 | 151 | def create_rotated_surface_codes(n, name=None): 152 | assert n % 2 == 1, "n should be odd" 153 | n2 = n*n 154 | m = (n2-1) // 2 155 | hx = np.zeros((m, n2), dtype=int) 156 | hz = np.zeros((m, n2), dtype=int) 157 | x_idx = 0 158 | z_idx = 0 159 | 160 | for i in range(n-1): 161 | for j in range(n-1): 162 | if (i+j) % 2 == 0: # Z check 163 | set_pcm_row(n, hz, z_idx, i, j) 164 | z_idx += 1 165 | else: # X check 166 | set_pcm_row(n, hx, x_idx, i, j) 167 | x_idx += 1 168 | 169 | # upper and lower edge, weight-2 X checks 170 | for j in range(n-1): 171 | if j % 2 == 0: # upper 172 | hx[x_idx][j] = hx[x_idx][j+1] = 1 173 | else: 174 | hx[x_idx][(n-1)*n+j] = hx[x_idx][(n-1)*n+(j+1)] = 1 175 | x_idx += 1 176 | 177 | # left and right edge, weight-2 Z checks 178 | for i in range(n-1): 179 | if i % 2 == 0: # right 180 | hz[z_idx][i*n+(n-1)] = hz[z_idx][(i+1)*n+(n-1)] = 1 181 | else: 182 | hz[z_idx][i*n] = hz[z_idx][(i+1)*n] = 1 183 | z_idx += 1 184 | 185 | return css_code(hx, hz, name=name, name_prefix="Rotated_Surface") 186 | 187 | def create_checkerboard_toric_codes(n, name=None): 188 | assert n % 2 == 0, "n should be even" 189 | n2 = n*n 190 | m = (n2) // 2 191 | hx = np.zeros((m, n2), dtype=int) 192 | hz = np.zeros((m, n2), dtype=int) 193 | x_idx = 0 194 | z_idx = 0 195 | 196 | for i in range(n): 197 | for j in range(n): 198 | if (i+j) % 2 == 0: # Z check 199 | set_pcm_row(n, hz, z_idx, i, j) 200 | z_idx += 1 201 | else: 202 | set_pcm_row(n, hx, x_idx, i, j) 203 | x_idx += 1 204 | 205 | return css_code(hx, hz, name=name, name_prefix="Toric") 206 | 207 | def create_QC_GHP_codes(l, a, b, name=None): 208 | # quasi-cyclic generalized hypergraph product codes 209 | m, n = a.shape 210 | block_list = [] 211 | for row in a: 212 | temp = [] 213 | for s in row: 214 | if s >= 0: 215 | temp.append(create_circulant_matrix(l, [s])) 216 | else: 217 | temp.append(np.zeros((l,l), dtype=int)) 218 | block_list.append(temp) 219 | A = np.block(block_list) # ml * nl 220 | 221 | temp_b = create_circulant_matrix(l, b) 222 | B = np.kron(np.identity(m, dtype=int), temp_b) 223 | hx = np.hstack((A, B)) 224 | B_T = np.kron(np.identity(n, dtype=int), temp_b.T) 225 | hz = np.hstack((B_T, A.T)) 226 | return css_code(hx, hz, name=name, name_prefix=f"GHP") 227 | 228 | def create_cyclic_permuting_matrix(n, shifts): 229 | A = np.full((n,n), -1, dtype=int) 230 | for i, s in enumerate(shifts): 231 | for j in range(n): 232 | A[j, (j-i)%n] = s 233 | return A 234 | 235 | def create_bivariate_bicycle_codes(l, m, A_x_pows, A_y_pows, B_x_pows, B_y_pows, name=None): 236 | S_l=create_circulant_matrix(l, [-1]) 237 | S_m=create_circulant_matrix(m, [-1]) 238 | x = kron(S_l, identity(m, dtype=int)) 239 | y = kron(identity(l, dtype=int), S_m) 240 | A_list = [x**p for p in A_x_pows] + [y**p for p in A_y_pows] 241 | B_list = [y**p for p in B_y_pows] + [x**p for p in B_x_pows] 242 | A = reduce(lambda x,y: x+y, A_list).toarray() 243 | B = reduce(lambda x,y: x+y, B_list).toarray() 244 | hx = np.hstack((A, B)) 245 | hz = np.hstack((B.T, A.T)) 246 | return css_code(hx, hz, name=name, name_prefix="BB", check_css=True), A_list, B_list 247 | 248 | # For reading in overcomplete check matrices 249 | def readAlist(directory): 250 | ''' 251 | Reads in a parity check matrix (pcm) in A-list format from text file. returns the pcm in form of a numpy array with 0/1 bits as float64. 252 | ''' 253 | alist_raw = [] 254 | with open(directory, "r") as f: 255 | lines = f.readlines() 256 | for line in lines: 257 | # remove trailing newline \n and split at spaces: 258 | line = line.rstrip().split(" ") 259 | # map string to int: 260 | line = list(map(int, line)) 261 | alist_raw.append(line) 262 | alist_numpy = alistToNumpy(alist_raw) 263 | alist_numpy = alist_numpy.astype(int) 264 | return alist_numpy 265 | 266 | 267 | def alistToNumpy(lines): 268 | '''Converts a parity-check matrix in AList format to a 0/1 numpy array''' 269 | nCols, nRows = lines[0] 270 | if len(lines[2]) == nCols and len(lines[3]) == nRows: 271 | startIndex = 4 272 | else: 273 | startIndex = 2 274 | matrix = np.zeros((nRows, nCols), dtype=float) 275 | for col, nonzeros in enumerate(lines[startIndex:startIndex + nCols]): 276 | for rowIndex in nonzeros: 277 | if rowIndex != 0: 278 | matrix[rowIndex - 1, col] = 1 279 | return matrix 280 | 281 | 282 | def multiply_elements(a_b, c_d, n, m, k): 283 | a, b = a_b 284 | c, d = c_d 285 | return ((a + c * pow(k, b, n)) % n, (b+d) % m) 286 | 287 | def idx2tuple(idx, m): 288 | b = idx % m 289 | a = (idx - b) / m 290 | return (a, b) 291 | 292 | def create_2BGA(n, m, k, a_poly, b_poly, sr=False): 293 | l = n*m 294 | A = np.zeros((l,l)) 295 | for (a,b) in a_poly: # convert s^a r^b to r^{b k^a} s^a 296 | if sr: 297 | x = b * pow(k, a, n) % n 298 | b = a 299 | a = x 300 | for i in range(l): 301 | c, d = idx2tuple(i, m) 302 | a_, b_ = multiply_elements((a,b), (c,d), n, m, k) 303 | idx = a_ * m + b_ 304 | A[int(idx), i] += 1 305 | 306 | A = A % 2 307 | 308 | B = np.zeros((l,l)) 309 | for (a,b) in b_poly: 310 | if sr: 311 | x = b * pow(k, a, n) % n 312 | b = a 313 | a = x 314 | for i in range(l): 315 | c, d = idx2tuple(i, m) 316 | a_, b_ = multiply_elements((c,d), (a,b), n, m, k) 317 | idx = a_ * m + b_ 318 | B[int(idx), i] += 1 319 | 320 | B = B % 2 321 | hx = np.hstack((A, B)) 322 | hz = np.hstack((B.T, A.T)) 323 | return css_code(hx, hz, name_prefix="2BGA", check_css=True) 324 | 325 | 326 | def find_girth(pcm): 327 | m, n = pcm.shape 328 | a1 = np.hstack((np.zeros((m,m)), pcm)) 329 | a2 = np.hstack((pcm.T, np.zeros((n,n)))) 330 | adj_matrix = np.vstack((a1,a2)) # adjacency matrix 331 | n = len(adj_matrix) 332 | girth = float('inf') # Initialize girth as infinity 333 | 334 | def bfs(start): 335 | nonlocal girth 336 | distance = [-1] * n # Distance from start to every other node 337 | distance[start] = 0 338 | queue = deque([start]) 339 | 340 | while queue: 341 | vertex = queue.popleft() 342 | for neighbor, is_edge in enumerate(adj_matrix[vertex]): 343 | if is_edge: 344 | if distance[neighbor] == -1: 345 | # Neighbor not visited, set distance and enqueue 346 | distance[neighbor] = distance[vertex] + 1 347 | queue.append(neighbor) 348 | elif distance[neighbor] >= distance[vertex] + 1: 349 | # Found a cycle, update girth if it's the shortest 350 | girth = min(girth, distance[vertex] + distance[neighbor] + 1) 351 | 352 | # Run BFS from every vertex to find the shortest cycle 353 | for i in range(n): 354 | bfs(i) 355 | 356 | return girth if girth != float('inf') else -1 # Return -1 if no cycle is found 357 | 358 | def gcd(f_coeff, g_coeff): 359 | return poly2coeff(gcd_inner(coeff2poly(f_coeff), coeff2poly(g_coeff))) 360 | 361 | def gcd_inner(f, g, p=2): 362 | if len(f) < len(g): 363 | return gcd_inner(g,f,p) 364 | 365 | r = [0] * len(f) 366 | r_mult = reciprocal(g[0], p)*f[0] 367 | 368 | for i in range(len(f)): 369 | if i < len(g): 370 | r[i] = f[i] - g[i] * r_mult 371 | else: 372 | r[i] = f[i] 373 | if p != 0: 374 | r[i] %= p 375 | 376 | while abs(r[0]) < 0.0001: 377 | r.pop(0) 378 | if (len(r) == 0): 379 | return g 380 | 381 | return gcd_inner(r, g, p) 382 | 383 | # Returns reciprocal of n in finite field of prime p, if p=0 returns 1/n 384 | def reciprocal(n, p=0): 385 | if p == 0: 386 | return 1/n 387 | for i in range(p): 388 | if (n*i) % p == 1: 389 | return i 390 | return None 391 | 392 | def coeff2poly(coeff): 393 | """Example: input [0,1,7], output [1,0,0,0,0,0,1,1] (coefficients in decreasing order of degree)""" 394 | lead = max(coeff) 395 | poly = np.zeros(lead+1, dtype=int) 396 | for i in coeff: 397 | poly[lead-i] = 1 398 | return list(poly) 399 | 400 | def poly2coeff(poly): 401 | l = len(poly) - 1 402 | return [l-i for i in range(l+1) if poly[i]][::-1] 403 | 404 | 405 | def create_cycle_assemble_codes(p, sigma): 406 | first_row = [pow(sigma, i, p) for i in range(p-1)] 407 | mat = np.zeros((p-1, p-1), dtype=int) 408 | mat[0, :] = first_row 409 | for i in range(1, p-1): 410 | mat[i, :] = np.roll(mat[i-1, :], 1) 411 | mat = np.hstack((np.ones((p-1,1)), mat)).astype(int) 412 | first_half = (p-1)//2 413 | block_list = [] 414 | for row in mat[:first_half]: 415 | temp = [] 416 | for s in row: 417 | temp.append(create_circulant_matrix(p, [-s])) 418 | block_list.append(temp) 419 | A = np.block(block_list) 420 | hx = np.hstack((A, np.ones((first_half*p,1)))) 421 | block_list = [] 422 | for row in mat[first_half:]: 423 | temp = [] 424 | for s in row: 425 | temp.append(create_circulant_matrix(p, [-s])) 426 | block_list.append(temp) 427 | B = np.block(block_list) 428 | hz = np.hstack((B, np.ones((first_half*p,1)))) 429 | return css_code(hx, hz, name_prefix=f"CAMEL", check_css=True) 430 | 431 | def strip_leading_zeros(poly): 432 | """Remove leading zeros from a polynomial represented as a list of coefficients.""" 433 | if not poly: 434 | return poly 435 | i = len(poly) - 1 436 | while i >= 0 and poly[i] == 0: 437 | i -= 1 438 | return poly[:i+1] 439 | 440 | def poly_divmod(a, b, p): 441 | """ 442 | Perform polynomial division a / b over the finite field F_p. 443 | Input: a and b both list of coefficients, in increasing order of degree 444 | Returns: Tuple of quotient and remainder polynomials, both as lists of coefficients. 445 | """ 446 | 447 | a = strip_leading_zeros(a) 448 | b = strip_leading_zeros(b) 449 | 450 | deg_a = len(a) - 1 451 | deg_b = len(b) - 1 452 | if deg_a < deg_b: 453 | return [0], a # quotient is zero, remainder is a 454 | 455 | inv_lead_b = pow(int(b[-1]), p-2, p) # inverse of leading coeff 456 | 457 | q = [0] * (deg_a - deg_b + 1) # initialize quotient with zeros 458 | r = a[:] # remainder starts as dividend 459 | 460 | while len(r) - 1 >= deg_b and any(r): 461 | deg_r = len(r) - 1 462 | lead_r = r[-1] # leading coeff of current remainder 463 | factor = (lead_r * inv_lead_b) % p # lead_r / lead_b 464 | shift = deg_r - deg_b 465 | q[shift] = factor 466 | for i in range(deg_b + 1): 467 | r[shift + i] = (r[shift + i] - factor * b[i]) % p 468 | 469 | r = strip_leading_zeros(r) 470 | 471 | # normalize quotient and remainder 472 | q = strip_leading_zeros(q) 473 | r = strip_leading_zeros(r) 474 | if not q: 475 | q = [0] 476 | if not r: 477 | r = [0] 478 | 479 | return q, r 480 | 481 | def multiply_polynomials(a, b, m, primitive_polynomial): 482 | """Multiply two polynomials modulo the primitive polynomial in GF(2^m).""" 483 | result = 0 484 | while b: 485 | if b & 1: 486 | result ^= a # Add a to the result if the lowest bit of b is 1 487 | b >>= 1 488 | a <<= 1 # Equivalent to multiplying a by x 489 | if a & (1 << m): 490 | a ^= primitive_polynomial # Reduce a modulo the primitive polynomial 491 | return result 492 | 493 | def generate_log_antilog_tables(m, primitive_polynomial): 494 | """Generate log and antilog tables for GF(2^m) using a given primitive polynomial.""" 495 | gf_size = 2**m 496 | log_table = [-1] * gf_size 497 | antilog_table = [0] * gf_size 498 | 499 | # Set the initial element 500 | alpha = 1 # alpha^0 501 | for i in range(gf_size - 1): 502 | antilog_table[i] = alpha 503 | log_table[alpha] = i 504 | 505 | # Multiply alpha by the primitive element, equivalent to "x" in polynomial representation 506 | alpha = multiply_polynomials(alpha, 2, m, primitive_polynomial) 507 | 508 | # Set log(0) separately as it's undefined, but we use -1 as a placeholder 509 | log_table[0] = -1 510 | 511 | return log_table, antilog_table 512 | 513 | 514 | def construct_vector(m, log_table, antilog_table): 515 | """Calculate for every i, the j such that alpha^j=1+alpha^i.""" 516 | gf_size = 2**m 517 | vector = [-1] * gf_size # Initialize vector 518 | 519 | for i in range(1, gf_size): # Skip 0 as alpha^0 = 1, and we are interested in alpha^i where i != 0 520 | # Calculate 1 + alpha^i in GF(2^m) 521 | # Since addition is XOR in GF(2^m), and alpha^0 = 1, we use log/antilog tables 522 | sum_val = 1 ^ antilog_table[i % (gf_size - 1)] # Note: antilog_table[log_val % (gf_size - 1)] == alpha^i 523 | 524 | if sum_val < gf_size and log_table[sum_val] != -1: 525 | vector[i] = log_table[sum_val] 526 | 527 | return vector 528 | 529 | def get_primitive_polynomial(m): 530 | # get a primitive polynomial for GF(2^m) 531 | # here I use the Conway polynomial, you can obtain it by installing the galois package 532 | # >>> import galois 533 | # >>> galois.conway_poly(2, 15) # for GF(2^15) 534 | # then convert it to the binary form 535 | if m == 2: 536 | primitive_polynomial = 0b111 537 | elif m == 3: 538 | primitive_polynomial = 0b1011 539 | elif m == 4: 540 | primitive_polynomial = 0b10011 541 | elif m == 6: 542 | primitive_polynomial = 0b1011011 543 | elif m == 8: 544 | primitive_polynomial = 0b100011101 545 | elif m == 9: 546 | primitive_polynomial = 0b1000010001 547 | elif m == 10: 548 | primitive_polynomial = 0b10001101111 549 | elif m == 12: 550 | primitive_polynomial = 0b1000011101011 551 | elif m == 15: 552 | primitive_polynomial = 0b1000000000110101 553 | else: 554 | raise ValueError(f"Unsupported m={m}, use the galois package to find the Conway polynomial yourself.") 555 | return primitive_polynomial 556 | 557 | def create_EG_codes(s): 558 | order = 2 ** (2*s) - 1 559 | extension = 2*s 560 | primitive_polynomial = get_primitive_polynomial(extension) 561 | log_table, antilog_table = generate_log_antilog_tables(extension, primitive_polynomial) 562 | vector = construct_vector(extension, log_table, antilog_table) 563 | 564 | # In GF(2^{2s}), beta = alpha^{2^s+1} generates GF(2^s) 565 | log_beta = 2 ** s + 1 566 | # A line is {alpha^i + beta*alpha^j} 567 | lines = [] 568 | for i in range(order): 569 | for j in range(log_beta): 570 | incidence_vec = np.zeros(2 ** (2*s)) 571 | # the zero-th is for 0, the {i+1}^th is for alpha^i 572 | incidence_vec[i+1] = 1 573 | 574 | for k in range(2 ** s): 575 | idx = (k * log_beta + j - i) % order 576 | if idx == 0: # add up to zero 577 | incidence_vec[0] = 1 578 | else: 579 | c = (i + vector[idx]) % order 580 | incidence_vec[c+1] = 1 581 | lines.append(incidence_vec) 582 | 583 | H = np.unique(np.array(lines).astype(bool), axis=0).T 584 | num_row, num_col = H.shape 585 | assert num_col == 2 ** (2*s) + 2 ** s 586 | hx = np.hstack((H, np.ones((num_row,1)))) 587 | hz = np.hstack((H, np.ones((num_row,1)))) 588 | return css_code(hx, hz, name_prefix=f"EG", check_css=True) 589 | -------------------------------------------------------------------------------- /src/include/COPYRIGHT: -------------------------------------------------------------------------------- 1 | Except as otherwise specified, all program code and documentation in this 2 | directory is copyright (c) 1995-2012 by Radford M. Neal. 3 | 4 | Permission is granted for anyone to copy, use, modify, and distribute 5 | these programs and accompanying documents for any purpose, provided 6 | this copyright notice is retained and prominently displayed, and note 7 | is made of any changes made to these programs. These programs and 8 | documents are distributed without any warranty, express or implied. 9 | As the programs were written for research purposes only, they have not 10 | been tested to the degree that would be advisable in any important 11 | application. All use of these programs is entirely at the user's own 12 | risk. 13 | 14 | Some routines in the module rand.c are taken from the GNU C Library, 15 | and are copyrighted as described there and in the file LGPL. 16 | -------------------------------------------------------------------------------- /src/include/README.md: -------------------------------------------------------------------------------- 1 | # Software for LDPC codes 2 | 3 | The `mod2parse.h` and `mod2sparse.c` are adapted from Radford Neal's [Software for Low Density Parity Check Codes](https://www.cs.toronto.edu/~radford/ftp/LDPC-2012-02-11/index.html) package which was published under an MIT open source license (see COPYRIGHT file). 4 | 5 | The following changes have been made to the origin files: 6 | 7 | ## Changes to `mod2sparse.h` 8 | - Header guards have been added. 9 | - The `mod2sparse.lr` attribute has been renamed to `mod2sparse.check_to_bit`. 10 | - The `mod2sparse.pr` attribute has been renamed to `mod2sparse.bit_to_check`. 11 | - Various functions relating to input/output in the origin file have been deleted. 12 | 13 | ## Changes to `mod2sparse.c` 14 | - The `chk_alloc` function has been moved to `mod2sparse.c` from `alloc.c`. 15 | - Various functions relating to input/output in the origin file have been deleted. -------------------------------------------------------------------------------- /src/include/bpgd.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BPGD_HPP 2 | #define BPGD_HPP 3 | extern "C" { 4 | #include "mod2sparse.h" 5 | } 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class BPGD { 13 | public: 14 | int m, n; 15 | int A, A_sum, C, D; 16 | int num_active_vn; 17 | mod2sparse* pcm; 18 | double* llr_prior; 19 | double** llr_posterior; 20 | char* vn_mask; 21 | char* vn_degree; 22 | char* cn_mask; 23 | char* cn_degree; 24 | char* error; 25 | char* syndrome; 26 | char* temp_syndrome; 27 | int num_iter; 28 | int low_error_mode; 29 | double factor; 30 | 31 | int min_sum_log(); 32 | void init(); 33 | int peel(); 34 | int vn_set_value(int vn, char value); 35 | int reset(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome); 36 | void set_masks(char* source_vn_mask, char* source_cn_mask, char* source_cn_degree); 37 | int select_vn(int depth, int& guess_vn); 38 | int decimate_vn_reliable(int depth, double fraction); 39 | void set_thresh(int A, int B, int C, int D) { this->A = A; this->A_sum = B; this->C = C; this->D = D; } 40 | double get_pm(); 41 | BPGD() {} 42 | BPGD(int m, int n, int num_iter, int low_error_mode, double factor); 43 | // Delete copy constructor and copy assignment operator 44 | // use smart_ptr to avoid double free 45 | BPGD(const BPGD&) = delete; 46 | BPGD& operator=(const BPGD&) = delete; 47 | ~BPGD(); 48 | }; 49 | 50 | class Barrier { 51 | public: 52 | Barrier(int count) : thread_count(count), count(count), generation(0) {} 53 | 54 | void wait() { 55 | std::unique_lock lock(mutex); 56 | int gen = generation; 57 | if (--count == 0) { 58 | generation++; 59 | count = thread_count; 60 | cv.notify_all(); 61 | } else { 62 | cv.wait(lock, [this, gen] { return gen != generation; }); 63 | } 64 | } 65 | 66 | private: 67 | std::mutex mutex; 68 | std::condition_variable cv; 69 | int thread_count; 70 | int count; 71 | int generation; 72 | }; 73 | 74 | class BPGD_tree_thread : public BPGD { 75 | public: 76 | bool converge; 77 | int status; 78 | int id; 79 | int finished; 80 | double min_pm; 81 | int backup_vn; 82 | int backup_value; 83 | int current_depth; 84 | int max_tree_depth; 85 | int max_step; 86 | std::vector backup_vn_mask; 87 | std::vector backup_cn_mask; 88 | std::vector backup_cn_degree; 89 | void do_work(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome, Barrier& barrier, 90 | std::mutex& store_mtx, double& pm, std::vector& min_pm_error); 91 | BPGD_tree_thread(int m, int n, int num_iter, int max_tree_depth, int max_step, int low_error_mode, double factor); 92 | BPGD_tree_thread() {} 93 | BPGD_tree_thread(const BPGD_tree_thread&) = delete; 94 | BPGD_tree_thread& operator=(const BPGD_tree_thread&) = delete; 95 | }; 96 | 97 | class BPGD_side_thread: public BPGD { 98 | public: 99 | bool converge; 100 | int status; 101 | int id; 102 | int finished; 103 | double min_pm; 104 | int backup_vn; 105 | int backup_value; 106 | int current_depth; 107 | int max_step; 108 | void do_work(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome, 109 | std::mutex& mtx, std::condition_variable& cv, Barrier& barrier, 110 | std::mutex& store_mtx, double& pm, std::vector& min_pm_error); 111 | BPGD_side_thread(int m, int n, int num_iter, int max_step, int low_error_mode, double factor): BPGD(m, n, num_iter, low_error_mode, factor), max_step(max_step) { set_thresh(0.0, -10.0, 30.0, 3.0); } 112 | BPGD_side_thread() {} 113 | BPGD_side_thread(const BPGD_side_thread&) = delete; 114 | BPGD_side_thread& operator=(const BPGD_side_thread&) = delete; 115 | }; 116 | 117 | class BPGD_main_thread: public BPGD { 118 | public: 119 | int max_step; // main thread max number of steps 120 | int max_tree_depth; // where tree thread splitting ends 121 | int max_tree_step; // tree threads, how many more steps after last splitting 122 | int max_side_depth; // where side thread splitting ends 123 | int max_side_step; // side threads, how many more steps after last splitting 124 | int num_tree_threads; 125 | int num_side_threads; 126 | double min_pm; 127 | std::vector min_pm_error; // avoid using dynamic memory allocation, avoid writing destructor, 128 | // requires special handling with move (assignment) constructor when used together with thread 129 | std::vector threads; 130 | // use smart pointers for class objects, to avoid writing destructor 131 | std::vector> bpgd_side_vec; 132 | std::vector> bpgd_tree_vec; 133 | void do_work(mod2sparse* source_pcm, int* copy_cols, double* source_llr_prior, char* source_syndrome); 134 | BPGD_main_thread(int m, int n, int num_iter, int max_step, int max_tree_depth, int max_side_depth, int max_tree_step, int max_side_step, int low_error_mode, double factor); 135 | BPGD_main_thread(int m, int n, int low_error_mode) : BPGD_main_thread(m, n, 6, 25, 3, 10, 10, 10, low_error_mode, 1.0) {} 136 | BPGD_main_thread() {} 137 | // Delete copy constructor and copy assignment to prevent copying 138 | BPGD_main_thread(const BPGD_main_thread&) = delete; 139 | BPGD_main_thread& operator=(const BPGD_main_thread&) = delete; 140 | // Optionally implement move constructor and move assignment operator 141 | BPGD_main_thread(BPGD_main_thread&&) noexcept = default; 142 | BPGD_main_thread& operator=(BPGD_main_thread&&) noexcept = default; 143 | 144 | }; 145 | 146 | void index_sort(double *v, int *cols, int N); 147 | void mod2sparse_mulvec_cpp(mod2sparse *m, char *u, char *v); 148 | void mod2sparse_free_cpp(mod2sparse *m); 149 | double logaddexp(double x, double y); 150 | double log1pexp(double x); 151 | 152 | #endif -------------------------------------------------------------------------------- /src/include/mod2sparse.h: -------------------------------------------------------------------------------- 1 | #ifndef MOD2SPARSE_H 2 | #define MOD2SPARSE_H 3 | 4 | /* MOD2SPARSE.H - Interface to module for handling sparse mod2 matrices. */ 5 | 6 | /* Copyright (c) 1995-2012 by Radford M. Neal. 7 | * 8 | * Permission is granted for anyone to copy, use, modify, and distribute 9 | * these programs and accompanying documents for any purpose, provided 10 | * this copyright notice is retained and prominently displayed, and note 11 | * is made of any changes made to these programs. These programs and 12 | * documents are distributed without any warranty, express or implied. 13 | * As the programs were written for research purposes only, they have not 14 | * been tested to the degree that would be advisable in any important 15 | * application. All use of these programs is entirely at the user's own 16 | * risk. 17 | */ 18 | 19 | 20 | /* This module implements operations on sparse matrices of mod2 elements 21 | (bits, with addition and multiplication being done modulo 2). 22 | 23 | All procedures in this module display an error message on standard 24 | error and terminate the program if passed an invalid argument (indicative 25 | of a programming error), or if memory cannot be allocated. Errors from 26 | invalid contents of a file result in an error code being returned to the 27 | caller, with no message being printed by this module. 28 | */ 29 | 30 | 31 | /* DATA STRUCTURES USED TO STORE A SPARSE MATRIX. Non-zero entries (ie, 1s) 32 | are represented by nodes that are doubly-linked both by row and by column, 33 | with the headers for these lists being kept in arrays. Nodes are allocated 34 | in blocks to reduce time and space overhead. Freed nodes are kept for 35 | reuse in the same matrix, rather than being freed for other uses, except 36 | that they are all freed when the matrix is cleared to all zeros by the 37 | mod2sparse_clear procedure, or copied into by mod2sparse_copy. 38 | 39 | Direct access to these structures should be avoided except in low-level 40 | routines. Use the macros and procedures defined below instead. */ 41 | 42 | 43 | void *chk_alloc (unsigned, unsigned); /* Calls 'calloc' and exits with error 44 | if it fails */ 45 | 46 | typedef struct mod2entry /* Structure representing a non-zero entry, or 47 | the header for a row or column */ 48 | { 49 | int row, col, sgn; /* Row and column indexes of this entry, starting 50 | at 0, and with -1 for a row or column header */ 51 | 52 | struct mod2entry *left, *right, /* Pointers to entries adjacent in row */ 53 | *up, *down; /* and column, or to headers. Free */ 54 | /* entries are linked by 'left'. */ 55 | 56 | double check_to_bit, bit_to_check; /* Probability and likelihood ratios - not used */ 57 | /* by the mod2sparse module itself */ 58 | } mod2entry; 59 | 60 | #define Mod2sparse_block 10 /* Number of entries to block together for 61 | memory allocation */ 62 | 63 | typedef struct mod2block /* Block of entries allocated all at once */ 64 | { 65 | struct mod2block *next; /* Next block that has been allocated */ 66 | 67 | mod2entry entry[Mod2sparse_block]; /* Entries in this block */ 68 | 69 | } mod2block; 70 | 71 | typedef struct /* Representation of a sparse matrix */ 72 | { 73 | int n_rows; /* Number of rows in the matrix */ 74 | int n_cols; /* Number of columns in the matrix */ 75 | 76 | mod2entry *rows; /* Pointer to array of row headers */ 77 | mod2entry *cols; /* Pointer to array of column headers */ 78 | 79 | mod2block *blocks; /* Blocks that have been allocated */ 80 | mod2entry *next_free; /* Next free entry */ 81 | 82 | } mod2sparse; 83 | 84 | 85 | /* MACROS TO GET AT ELEMENTS OF A SPARSE MATRIX. The 'first', 'last', 'next', 86 | and 'prev' macros traverse the elements in a row or column. Moving past 87 | the first/last element gets one to a header element, which can be identified 88 | using the 'at_end' macro. Macros also exist for finding out the row 89 | and column of an entry, and for finding out the dimensions of a matrix. */ 90 | 91 | #define mod2sparse_first_in_row(m,i) ((m)->rows[i].right) /* Find the first */ 92 | #define mod2sparse_first_in_col(m,j) ((m)->cols[j].down) /* or last entry in */ 93 | #define mod2sparse_last_in_row(m,i) ((m)->rows[i].left) /* a row or column */ 94 | #define mod2sparse_last_in_col(m,j) ((m)->cols[j].up) 95 | 96 | #define mod2sparse_next_in_row(e) ((e)->right) /* Move from one entry to */ 97 | #define mod2sparse_next_in_col(e) ((e)->down) /* another in any of the four */ 98 | #define mod2sparse_prev_in_row(e) ((e)->left) /* possible directions */ 99 | #define mod2sparse_prev_in_col(e) ((e)->up) 100 | 101 | #define mod2sparse_at_end(e) ((e)->row<0) /* See if we've reached the end */ 102 | 103 | #define mod2sparse_row(e) ((e)->row) /* Find out the row or column index */ 104 | #define mod2sparse_col(e) ((e)->col) /* of an entry (indexes start at 0) */ 105 | 106 | #define mod2sparse_rows(m) ((m)->n_rows) /* Get the number of rows or columns*/ 107 | #define mod2sparse_cols(m) ((m)->n_cols) /* in a matrix */ 108 | 109 | 110 | /* POSSIBLE LU DECOMPOSITION STRATEGIES. For use with mod2sparse_decomp. */ 111 | 112 | typedef enum 113 | { Mod2sparse_first, 114 | Mod2sparse_mincol, 115 | Mod2sparse_minprod 116 | } mod2sparse_strategy; 117 | 118 | 119 | /* PROCEDURES TO MANIPULATE SPARSE MATRICES. */ 120 | 121 | mod2sparse *mod2sparse_allocate (int, int); 122 | void mod2sparse_free (mod2sparse *); 123 | 124 | void mod2sparse_clear (mod2sparse *); 125 | void mod2sparse_copy (mod2sparse *, mod2sparse *); 126 | void mod2sparse_copyrows (mod2sparse *, mod2sparse *, int *); 127 | void mod2sparse_copycols (mod2sparse *, mod2sparse *, int *); 128 | 129 | void mod2sparse_print (FILE *, mod2sparse *); 130 | int mod2sparse_write (FILE *, mod2sparse *); 131 | mod2sparse *mod2sparse_read (FILE *); 132 | 133 | mod2entry *mod2sparse_find (mod2sparse *, int, int); 134 | mod2entry *mod2sparse_insert (mod2sparse *, int, int); 135 | void mod2sparse_delete (mod2sparse *, mod2entry *); 136 | 137 | void mod2sparse_transpose (mod2sparse *, mod2sparse *); 138 | void mod2sparse_add (mod2sparse *, mod2sparse *, mod2sparse *); 139 | void mod2sparse_multiply (mod2sparse *, mod2sparse *, mod2sparse *); 140 | void mod2sparse_mulvec (mod2sparse *, char *, char *); 141 | 142 | int mod2sparse_equal (mod2sparse *, mod2sparse *); 143 | 144 | int mod2sparse_count_row (mod2sparse *, int); 145 | int mod2sparse_count_col (mod2sparse *, int); 146 | 147 | void mod2sparse_add_row (mod2sparse *, int, mod2sparse *, int); 148 | void mod2sparse_add_col (mod2sparse *, int, mod2sparse *, int); 149 | 150 | int mod2sparse_decomp (mod2sparse *, int, mod2sparse *, mod2sparse *, 151 | int *, int *, mod2sparse_strategy, int, int); 152 | 153 | int mod2sparse_forward_sub (mod2sparse *, int *, char *, char *); 154 | int mod2sparse_backward_sub (mod2sparse *, int *, char *, char *); 155 | 156 | #endif /* MOD2SPARSE_H */ 157 | -------------------------------------------------------------------------------- /src/include/mod2sparse_extra.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "mod2sparse_extra.hpp" 7 | 8 | char* decimal_to_binary_reverse(int n, int k) { // memory leak!! 9 | char* binary_number; 10 | int divisor; 11 | int remainder; 12 | divisor = n; 13 | binary_number = (char*) chk_alloc(k, sizeof(char *)); 14 | for(int i=0; iN) ? N : M; // min(M, N) 51 | 52 | L = mod2sparse_allocate(M, submatrix_size); 53 | U = mod2sparse_allocate(submatrix_size, N); 54 | 55 | mod2sparse_strategy strategy = Mod2sparse_first; /* Strategy to follow in picking rows/columns */ 56 | 57 | nnf = mod2sparse_decomp( 58 | A, /* Input matrix, M by N */ 59 | submatrix_size, /* Size of sub-matrix to find LU decomposition of */ 60 | L, /* Matrix in which L is stored, M by R */ 61 | U, /* Matrix in which U is stored, R by N */ 62 | rows, /* Array where row indexes are stored, M long */ 63 | cols, /* Array where column indexes are stored, N long */ 64 | strategy, 65 | abandon_number, 66 | abandon_when); 67 | 68 | free(rows); 69 | free(cols); 70 | mod2sparse_free(L); 71 | mod2sparse_free(U); 72 | 73 | rank = submatrix_size - nnf; 74 | 75 | return rank; 76 | } 77 | 78 | void LU_forward_backward_solve( 79 | mod2sparse *L, 80 | mod2sparse *U, 81 | int *rows, 82 | int *cols, 83 | char *z, 84 | char *x) { 85 | int N, R; 86 | char* forward_b; 87 | N = mod2sparse_cols(U); 88 | R = mod2sparse_cols(L); 89 | forward_b = (char*) chk_alloc(R, sizeof(*forward_b)); 90 | 91 | for(int bit_no=0; bit_no N-R) { 143 | fprintf(stderr, "Trying to abandon more columns than allowed\n"); 144 | exit(1); 145 | } 146 | 147 | rinv = (int*) chk_alloc(M, sizeof *rinv); 148 | cinv = (int*) chk_alloc(N, sizeof *cinv); 149 | 150 | if (abandon_number>0) 151 | acnt = (int*) chk_alloc(M+1, sizeof *acnt); 152 | 153 | if (strategy==Mod2sparse_minprod) 154 | rcnt = (int*) chk_alloc(M, sizeof *rcnt); 155 | 156 | //these are the problematic functions! 157 | mod2sparse_clear(L); 158 | mod2sparse_clear(U); 159 | 160 | /* Copy A to B. B will be modified, then discarded. */ 161 | B = mod2sparse_allocate(M, N); 162 | mod2sparse_copy(A, B); 163 | 164 | /* Count 1s in rows of B, if using minprod strategy. */ 165 | if (strategy==Mod2sparse_minprod) { 166 | for (i=0; i= i) { 190 | found = 1; 191 | goto out_first; 192 | } 193 | e = mod2sparse_next_in_col(e); 194 | } 195 | } 196 | out_first: 197 | break; 198 | } 199 | 200 | case Mod2sparse_mincol: 201 | { 202 | found = 0; 203 | 204 | for (j=i; j=i) { 210 | found = 1; 211 | cc = cc2; 212 | e = e2; 213 | k = j; 214 | break; 215 | } 216 | e2 = mod2sparse_next_in_col(e2); 217 | } 218 | } 219 | } 220 | 221 | break; 222 | } 223 | 224 | case Mod2sparse_minprod: 225 | { 226 | found = 0; 227 | for (j = i; j= i) { 232 | cr2 = rcnt[mod2sparse_row(e2)]; 233 | if (!found || cc2==1 || (cc2-1)*(cr2-1)i) { 294 | mod2sparse_add_row(B,k,B,mod2sparse_row(e)); 295 | if (strategy == Mod2sparse_minprod) { 296 | rcnt[k] = mod2sparse_count_row(B,k); 297 | } 298 | mod2sparse_insert(L,k,i); 299 | } 300 | else if (rinv[k]0 && i==abandon_when) { 322 | for (k = 0; kk || cc3==k && cc>0) { 341 | if (cc3==k) cc -= 1; 342 | for (;;) { 343 | f = mod2sparse_first_in_col(B,j); 344 | if (mod2sparse_at_end(f)) break; 345 | mod2sparse_delete(B,f); 346 | } 347 | cc2 += 1; 348 | } 349 | } 350 | 351 | if (cc2!=abandon_number) abort(); 352 | 353 | if (strategy==Mod2sparse_minprod) { 354 | for (j = 0; j0) free(acnt); 374 | 375 | return nnf; 376 | } 377 | 378 | 379 | void mod2sparse_merge_vec(mod2sparse* m1, char *vec, int n, mod2sparse* m2) { 380 | 381 | if (mod2sparse_cols(m2) < mod2sparse_cols(m1) + 1 || mod2sparse_rows(m2) < mod2sparse_rows(m1) || mod2sparse_rows(m2) < n) { 382 | printf("mod2sparse_merge_vec: the receiving matrix doesn't have the good dimensions"); 383 | } 384 | 385 | mod2sparse_copy(m1,m2); 386 | 387 | int ncols = mod2sparse_cols(m1); 388 | 389 | for (int i = 0; i < n; i++){ 390 | if (vec[i] != 0){ 391 | mod2sparse_insert(m2,i,ncols); 392 | } 393 | } 394 | 395 | } -------------------------------------------------------------------------------- /src/include/mod2sparse_extra.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MOD2SPARSE_EXTRA 2 | #define MOD2SPARSE_EXTRA 3 | extern "C" { 4 | #include "mod2sparse.h" 5 | } 6 | char* decimal_to_binary_reverse(int n, int K); 7 | 8 | int mod2sparse_rank(mod2sparse *A); 9 | 10 | void LU_forward_backward_solve 11 | (mod2sparse *L, 12 | mod2sparse *U, 13 | int *rows, 14 | int *cols, 15 | char *z, 16 | char *x); 17 | 18 | int mod2sparse_decomp_osd 19 | ( mod2sparse *A, /* Input matrix, M by N */ 20 | int R, /* Size of sub-matrix to find LU decomposition of */ 21 | mod2sparse *L, /* Matrix in which L is stored, M by R */ 22 | mod2sparse *U, /* Matrix in which U is stored, R by N */ 23 | int *rows, /* Array where row indexes are stored, M long */ 24 | int *cols /* Array where column indexes are stored, N long */ 25 | ); 26 | 27 | void mod2sparse_merge_vec(mod2sparse* m1, char *vec, int n, mod2sparse* m2); 28 | 29 | mod2sparse* mod2sparse_allocate_cpp (int u, int v); 30 | void mod2sparse_copycols_cpp (mod2sparse* m1, mod2sparse* m2, int* cols); 31 | 32 | #endif -------------------------------------------------------------------------------- /src/mod2sparse.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | from libc.stdlib cimport malloc, calloc,free 3 | import numpy as np 4 | cimport numpy as np 5 | 6 | from .c_util cimport numpy2double, numpy2char, char2numpy, double2numpy 7 | 8 | cdef extern from "mod2sparse.h": 9 | 10 | ctypedef struct mod2entry: 11 | int row, col, sgn 12 | double check_to_bit, bit_to_check 13 | 14 | ctypedef struct mod2sparse: 15 | int n_rows 16 | int n_cols 17 | 18 | mod2entry *rows 19 | mod2entry *cols 20 | 21 | cdef mod2sparse *mod2sparse_allocate(int n_rows, int n_cols) 22 | cdef mod2entry *mod2sparse_insert(mod2sparse *m,int row,int col) 23 | cdef void mod2sparse_free(mod2sparse *m) 24 | cdef void mod2sparse_mulvec(mod2sparse *m, char *a, char *b) 25 | cdef void mod2sparse_copycols(mod2sparse *A, mod2sparse *B, int *cols) 26 | 27 | cdef mod2entry* mod2sparse_first_in_row(mod2sparse *m, int i) 28 | cdef mod2entry* mod2sparse_first_in_col(mod2sparse *m, int j) 29 | cdef mod2entry* mod2sparse_last_in_row(mod2sparse *m, int i) 30 | cdef mod2entry* mod2sparse_last_in_col(mod2sparse *m, int j) 31 | cdef mod2entry* mod2sparse_next_in_row(mod2entry *e) 32 | cdef mod2entry* mod2sparse_next_in_col(mod2entry *e) 33 | cdef mod2entry* mod2sparse_prev_in_row(mod2entry *e) 34 | cdef mod2entry* mod2sparse_prev_in_col(mod2entry *e) 35 | cdef int mod2sparse_at_end(mod2entry *e) 36 | cdef int mod2sparse_row(mod2entry *e) 37 | cdef int mod2sparse_col(mod2entry *e) 38 | cdef int mod2sparse_rows(mod2sparse *m) 39 | cdef int mod2sparse_cols(mod2sparse *m) 40 | cdef int MEM_ALLOCATED 41 | 42 | 43 | cdef mod2sparse* numpy2mod2sparse(mat) 44 | cdef mod2sparse* spmatrix2mod2sparse(mat) 45 | cdef mod2sparse* alist2mod2sparse(fname) 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/mod2sparse.pyx: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | 3 | from scipy.sparse import spmatrix 4 | import numpy as np 5 | 6 | cdef mod2sparse* numpy2mod2sparse(mat): 7 | 8 | cdef mod2sparse* sparse_mat 9 | cdef int i,j,m,n 10 | m, n = mat.shape 11 | sparse_mat = mod2sparse_allocate(m, n) 12 | 13 | for i in range(m): 14 | for j in range(n): 15 | if mat[i,j]: 16 | mod2sparse_insert(sparse_mat, i, j) 17 | 18 | return sparse_mat 19 | 20 | cdef mod2sparse* spmatrix2mod2sparse(mat): 21 | 22 | cdef mod2sparse* sparse_mat 23 | cdef int i,j,n_rows,n_cols 24 | m, n = mat.shape 25 | sparse_mat=mod2sparse_allocate(m, n) 26 | 27 | for i, j in zip(*mat.nonzero()): 28 | mod2sparse_insert(sparse_mat, i, j) 29 | 30 | return sparse_mat 31 | 32 | cdef mod2sparse* alist2mod2sparse(fname): 33 | 34 | cdef mod2sparse* sparse_mat 35 | 36 | alist_file = np.loadtxt(fname, delimiter='\n', dtype=str) 37 | matrix_dimensions = alist_file[0].split() 38 | m = int(matrix_dimensions[0]) 39 | n = int(matrix_dimensions[1]) 40 | 41 | sparse_mat = mod2sparse_allocate(m, n) 42 | 43 | for i in range(m): 44 | for item in alist_file[i+4].split(): 45 | if item.isdigit(): 46 | column_index = int(item) 47 | mod2sparse_insert(sparse_mat, i, column_index) 48 | 49 | return sparse_mat -------------------------------------------------------------------------------- /src/osd_window.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True 2 | cimport numpy as np 3 | cimport cython 4 | from libc.stdlib cimport malloc, calloc, free 5 | from libc.math cimport log, tanh, isnan, abs 6 | 7 | from .mod2sparse cimport * 8 | from .c_util cimport numpy2char, char2numpy, numpy2double, double2numpy, spmatrix2char, numpy2int 9 | 10 | cdef extern from "bpgd.hpp": 11 | cdef void index_sort(double *soft_decisions, int *cols, int N) 12 | cdef void mod2sparse_mulvec_cpp(mod2sparse *m, char *u, char *v) 13 | cdef void mod2sparse_free_cpp(mod2sparse *m) 14 | 15 | cdef extern from "mod2sparse_extra.hpp": 16 | cdef void mod2sparse_print_terminal (mod2sparse *A) 17 | cdef int mod2sparse_rank(mod2sparse *A) 18 | cdef mod2sparse* mod2sparse_allocate_cpp (int u, int v) 19 | cdef void mod2sparse_copycols_cpp (mod2sparse* m1, mod2sparse* m2, int* cols) 20 | cdef char* decimal_to_binary_reverse(int n, int K) 21 | 22 | cdef void LU_forward_backward_solve( 23 | mod2sparse *L, 24 | mod2sparse *U, 25 | int *rows, 26 | int *cols, 27 | char *z, 28 | char *x) 29 | 30 | cdef int mod2sparse_decomp_osd( 31 | mod2sparse *A, 32 | int R, 33 | mod2sparse *L, 34 | mod2sparse *U, 35 | int *rows, 36 | int *cols) 37 | 38 | cdef class osd_window: 39 | cdef mod2sparse* H 40 | cdef int m, n, new_n 41 | cdef char* synd 42 | cdef char* bp_decoding_synd 43 | cdef char* bp_decoding 44 | cdef double* channel_llr 45 | 46 | 47 | cdef double** log_prob_ratios 48 | cdef char* current_vn 49 | cdef char* current_cn 50 | cdef char* cn_degree 51 | cdef char* current_cn_degree 52 | cdef int* cols 53 | cdef double* llr_sum 54 | cdef int history_length, bp_iteration 55 | 56 | cdef double min_pm 57 | cdef int converge 58 | 59 | cdef int pre_max_iter, post_max_iter 60 | 61 | cdef double ms_scaling_factor 62 | cdef int MEM_ALLOCATED 63 | 64 | # OSD 65 | cdef char* osd0_decoding 66 | cdef char* osdw_decoding 67 | cdef char** osdw_encoding_inputs 68 | cdef long int encoding_input_count 69 | cdef int osd_order 70 | cdef int osd_method 71 | cdef int rank 72 | cdef int k 73 | cdef int* rows 74 | cdef int* orig_cols 75 | cdef int* Ht_cols 76 | cdef char* y 77 | cdef char* g 78 | cdef char* Htx 79 | cdef void osd_e_setup(self) 80 | cdef void osd_cs_setup(self) 81 | cdef int osd(self) 82 | 83 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector) 84 | cdef void reset(self) 85 | cdef int peel(self) 86 | cdef int vn_set_value(self, vn, value) 87 | cdef int bp_decode_llr(self, max_iter) 88 | cdef void bp_init(self) 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/osd_window.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, embedsignature=True 2 | import numpy as np 3 | from scipy.sparse import spmatrix 4 | # from libcpp.vector cimport vector 5 | 6 | cdef class osd_window: 7 | 8 | def __cinit__(self, parity_check_matrix, **kwargs): 9 | 10 | channel_probs = kwargs.get("channel_probs") 11 | pre_max_iter = kwargs.get("pre_max_iter", 8) # BP preprocessing on original window PCM 12 | post_max_iter = kwargs.get("post_max_iter", 100) # BP postprocessing on shortened window PCM 13 | ms_scaling_factor = kwargs.get("ms_scaling_factor", 1.0) 14 | new_n = kwargs.get("new_n", None) 15 | osd_method = kwargs.get("osd_method", "osd_0") 16 | osd_order = kwargs.get("osd_order", 0) 17 | 18 | self.MEM_ALLOCATED = False 19 | 20 | if isinstance(parity_check_matrix, np.ndarray) or isinstance(parity_check_matrix, spmatrix): 21 | pass 22 | else: 23 | raise TypeError(f"The input matrix is of an invalid type. Please input a np.ndarray or scipy.sparse.spmatrix object, not {type(parity_check_matrix)}") 24 | 25 | self.m, self.n = parity_check_matrix.shape 26 | 27 | if channel_probs[0] != None: 28 | if len(channel_probs) != self.n: 29 | raise ValueError(f"The length of the channel probability vector must be eqaul to the block length n={self.n}.") 30 | 31 | #BP Settings 32 | self.pre_max_iter = pre_max_iter 33 | self.post_max_iter = post_max_iter 34 | self.ms_scaling_factor = ms_scaling_factor 35 | 36 | #memory allocation 37 | if isinstance(parity_check_matrix, np.ndarray): 38 | self.H = numpy2mod2sparse(parity_check_matrix) #parity check matrix in sparse form 39 | elif isinstance(parity_check_matrix, spmatrix): 40 | self.H = spmatrix2mod2sparse(parity_check_matrix) 41 | 42 | assert self.n == self.H.n_cols # validate number of bits in mod2sparse format 43 | assert self.m == self.H.n_rows # validate number of checks in mod2sparse format 44 | self.current_vn = calloc(self.n, sizeof(char)) # 0, 1 stands for decided value, -1 indicates undecided 45 | self.current_cn = calloc(self.m, sizeof(char)) # 0, 1 stands for active checks, -1 indicates already cleared 46 | self.cn_degree = calloc(self.m, sizeof(char)) # degree of each CN of the original PCM 47 | self.current_cn_degree = calloc(self.m, sizeof(char)) # current degree of each CN, for faster peeling 48 | # CN degree should be small, in particular, < 255, therefore use char instead of int 49 | self.synd = calloc(self.m, sizeof(char)) # syndrome string 50 | self.bp_decoding_synd = calloc(self.m, sizeof(char)) # decoded syndrome string 51 | self.bp_decoding = calloc(self.n, sizeof(char)) # BP decoding 52 | self.channel_llr = calloc(self.n, sizeof(double)) # channel probs 53 | self.log_prob_ratios = calloc(self.n, sizeof(double*)) # log probability ratios history 54 | self.history_length = 4 # BP posterior LLR history length 55 | for vn in range(self.n): 56 | self.log_prob_ratios[vn] = calloc(self.history_length, sizeof(double)) 57 | self.llr_sum = calloc(self.n, sizeof(double)) 58 | # things for post-processing 59 | self.cols = calloc(self.n, sizeof(int)) # for index sort according to BP posterior LLR 60 | if new_n is None: 61 | self.new_n = min(self.n, self.m * 2) 62 | else: 63 | self.new_n = min(new_n, self.n) 64 | 65 | 66 | # OSD 67 | self.osd0_decoding = calloc(self.n,sizeof(char)) #the OSD_0 decoding 68 | self.osdw_decoding = calloc(self.n,sizeof(char)) #the osd_w decoding 69 | if str(osd_method).lower() in ['OSD_0','osd_0','0','osd0']: 70 | osd_method = 0 71 | osd_order = 0 72 | elif str(osd_method).lower() in ['osd_e','1','osde','exhaustive','e']: 73 | osd_method = 1 74 | if osd_order > 15: 75 | print("WARNING: Running the 'OSD_E' (Exhaustive method) with search depth greater than 15 is not recommended. Use the 'osd_cs' method instead.") 76 | elif str(osd_method).lower() in ['osd_cs','2','osdcs','combination_sweep','combination_sweep','cs']: 77 | osd_method = 2 78 | else: 79 | raise ValueError(f"ERROR: OSD method '{osd_method}' invalid. Please choose from the following methods: 'OSD_0', 'OSD_E' or 'OSD_CS'.") 80 | self.osd_order = int(osd_order) 81 | self.osd_method = int(osd_method) 82 | 83 | 84 | self.encoding_input_count = 0 85 | 86 | if self.osd_order > -1: 87 | self.rank = mod2sparse_rank(self.H) 88 | try: 89 | assert self.osd_order <= (self.new_n - self.rank) 90 | except AssertionError: 91 | self.osd_order = -1 92 | raise ValueError(f"For this code, the OSD order should be set in the range 0<=osd_oder<={self.new_n - self.rank}.") 93 | self.orig_cols = calloc(self.n, sizeof(int)) 94 | self.rows = calloc(self.m, sizeof(int)) 95 | self.k = self.new_n - self.rank 96 | 97 | if self.osd_order > 0: 98 | self.y = calloc(self.n, sizeof(char)) 99 | self.g = calloc(self.m, sizeof(char)) 100 | self.Htx = calloc(self.m, sizeof(char)) 101 | self.Ht_cols = calloc(self.k, sizeof(int)) 102 | 103 | if osd_order == 0: pass 104 | elif self.osd_order > 0 and self.osd_method == 1: self.osd_e_setup() 105 | elif self.osd_order > 0 and self.osd_method == 2: self.osd_cs_setup() 106 | elif self.osd_order == -1: pass 107 | else: raise Exception(f"ERROR: OSD method '{osd_method}' invalid") 108 | 109 | 110 | self.MEM_ALLOCATED=True 111 | 112 | if channel_probs[0] != None: # convert probability to log-likelihood ratio (LLR) 113 | for vn in range(self.n): self.channel_llr[vn] = log((1-channel_probs[vn]) / channel_probs[vn]) 114 | 115 | cdef char deg = 0 116 | for cn in range(self.m): 117 | # must guarantee each CN has deg > 0 in numpy preprocessing 118 | e = mod2sparse_first_in_row(self.H, cn) 119 | deg = 0 120 | while not mod2sparse_at_end(e): 121 | deg += 1 122 | e = mod2sparse_next_in_row(e) 123 | self.cn_degree[cn] = deg 124 | 125 | self.min_pm = 0.0 # minimum BP path metric 126 | self.bp_iteration = 0 127 | 128 | cdef void osd_e_setup(self): 129 | self.encoding_input_count = long(2 ** self.osd_order) 130 | self.osdw_encoding_inputs = calloc(self.encoding_input_count, sizeof(char*)) 131 | for i in range(self.encoding_input_count): 132 | self.osdw_encoding_inputs[i] = decimal_to_binary_reverse(i, self.new_n - self.rank) 133 | 134 | cdef void osd_cs_setup(self): 135 | cdef int kset_size = self.new_n - self.rank 136 | assert self.osd_order <= kset_size 137 | self.encoding_input_count = kset_size + self.osd_order * (self.osd_order-1) / 2 138 | self.osdw_encoding_inputs = calloc(self.encoding_input_count, sizeof(char*)) 139 | cdef int total_count = 0 140 | for i in range(kset_size): 141 | self.osdw_encoding_inputs[total_count] = calloc(kset_size, sizeof(char)) 142 | self.osdw_encoding_inputs[total_count][i] = 1 143 | total_count += 1 144 | 145 | for i in range(self.osd_order): 146 | for j in range(self.osd_order): 147 | if i < j: 148 | self.osdw_encoding_inputs[total_count] = calloc(kset_size, sizeof(char)) 149 | self.osdw_encoding_inputs[total_count][i] = 1 150 | self.osdw_encoding_inputs[total_count][j] = 1 151 | total_count += 1 152 | 153 | # print("rank", self.rank) 154 | # print("total count osd CS", total_count) 155 | assert total_count == self.encoding_input_count 156 | 157 | 158 | cpdef np.ndarray[np.int_t, ndim=1] decode(self, input_vector): 159 | cdef int input_length = input_vector.shape[0] 160 | cdef int vn 161 | 162 | if input_length == self.m: 163 | self.synd = numpy2char(input_vector, self.synd) 164 | self.reset() 165 | self.bp_init() 166 | if self.bp_decode_llr(self.pre_max_iter): # check if preprocessing converged 167 | self.converge = True 168 | for vn in range(self.n): 169 | if self.bp_decoding[vn]: self.min_pm += self.channel_llr[vn] 170 | return char2numpy(self.bp_decoding, self.n) 171 | else: 172 | for vn in range(self.n): 173 | history = self.log_prob_ratios[vn] 174 | self.llr_sum[vn] = history[0] + history[1] + history[2] + history[3] 175 | 176 | index_sort(self.llr_sum, self.cols, self.n) 177 | 178 | for vn in range(self.new_n, self.n): 179 | if self.vn_set_value(self.cols[vn], 0) == -1: # decimation failed due to contradiction 180 | print("setting vn failed") 181 | return char2numpy(self.bp_decoding, self.n) 182 | for vn in range(self.new_n, self.n): 183 | self.bp_decoding[self.cols[vn]] = 0 184 | if self.peel() == -1: # peeling failed due to contradiction 185 | print("peeling failed") 186 | return char2numpy(self.bp_decoding, self.n) 187 | self.bp_init() 188 | if self.bp_decode_llr(self.post_max_iter): 189 | self.converge = True 190 | for vn in range(self.n): 191 | if self.bp_decoding[vn]: self.min_pm += self.channel_llr[vn] 192 | return char2numpy(self.bp_decoding, self.n) 193 | elif self.osd_order > -1: 194 | self.osd() 195 | return char2numpy(self.osdw_decoding, self.n) 196 | else: 197 | raise ValueError(f"The input to the ldpc.bp_decoder.decode must be a syndrome (of length={self.m}). The inputted vector has length={input_length}. Valid formats are `np.ndarray` or `scipy.sparse.spmatrix`.") 198 | 199 | return char2numpy(self.bp_decoding, self.n) 200 | 201 | cdef int osd(self): 202 | cdef int vn, cn 203 | cdef int history_idx = (self.post_max_iter - 1) % self.history_length 204 | 205 | for vn in range(self.n): 206 | if self.current_vn[vn] == 1: 207 | self.llr_sum[vn] = -1000 208 | elif self.current_vn[vn] == 0: 209 | self.llr_sum[vn] = 1000 210 | else: # not yet decided 211 | history = self.log_prob_ratios[vn] 212 | # self.llr_sum[vn] = history[history_idx] # not as good as sum 213 | self.llr_sum[vn] = history[0] + history[1] + history[2] + history[3] 214 | 215 | index_sort(self.llr_sum, self.cols, self.n) 216 | 217 | cdef int i, j 218 | cdef mod2sparse* L 219 | cdef mod2sparse* U 220 | L = mod2sparse_allocate_cpp(self.m, self.rank) 221 | U = mod2sparse_allocate_cpp(self.rank, self.n) 222 | 223 | for vn in range(self.n): 224 | self.orig_cols[vn] = self.cols[vn] 225 | 226 | # find the LU decomposition of the ordered matrix 227 | mod2sparse_decomp_osd(self.H, self.rank, L, U, self.rows, self.cols) 228 | # solve the syndrome equation with most probable full-rank submatrix 229 | LU_forward_backward_solve(L, U, self.rows, self.cols, self.synd, self.osd0_decoding) 230 | 231 | self.min_pm = 0.0 232 | # calculate pm for osd0_decoding 233 | for vn in range(self.n): 234 | if self.osd0_decoding[vn]: self.min_pm += self.channel_llr[vn] 235 | self.osdw_decoding[vn] = self.osd0_decoding[vn] # in case no higher order solution has a smaller pm than osd0 236 | 237 | if self.osd_order == 0: 238 | mod2sparse_free_cpp(U) 239 | mod2sparse_free_cpp(L) 240 | return 1 241 | 242 | # return the columns outside of the information set to their original ordering (the LU decomp scrambles them) 243 | cdef int counter=0, in_pivot 244 | cdef mod2sparse* Ht = mod2sparse_allocate_cpp(self.m, self.k) 245 | for i in range(self.new_n): 246 | cn = self.orig_cols[i] 247 | in_pivot = 0 248 | for j in range(self.rank): 249 | if self.cols[j] == cn: 250 | in_pivot = 1 251 | break 252 | if in_pivot == 0: 253 | self.cols[counter+self.rank] = cn 254 | counter += 1 255 | for i in range(self.k): 256 | self.Ht_cols[i] = self.cols[i + self.rank] 257 | # copy into the ordered, full-rank matrix Ht 258 | mod2sparse_copycols_cpp(self.H, Ht, self.Ht_cols) 259 | 260 | cdef char* x 261 | cdef long int l 262 | cdef double pm = 0.0 263 | for l in range(self.encoding_input_count): 264 | x = self.osdw_encoding_inputs[l] 265 | # subtract syndrome caused by x, get new syndrome for the syndrome equation 266 | mod2sparse_mulvec_cpp(Ht, x, self.Htx) 267 | for cn in range(self.m): 268 | self.g[cn] = self.synd[cn] ^ self.Htx[cn] 269 | 270 | LU_forward_backward_solve(L, U, self.rows, self.cols, self.g, self.y) 271 | for vn in range(self.k): 272 | self.y[self.Ht_cols[vn]] = x[vn] 273 | pm = 0.0 274 | for vn in range(self.n): 275 | if self.y[vn]: pm += self.channel_llr[vn] 276 | if pm < self.min_pm: 277 | self.min_pm = pm 278 | for vn in range(self.n): 279 | self.osdw_decoding[vn] = self.y[vn] 280 | 281 | mod2sparse_free_cpp(Ht) 282 | mod2sparse_free_cpp(U) 283 | mod2sparse_free_cpp(L) 284 | return 1 285 | 286 | 287 | 288 | cdef void reset(self): 289 | self.bp_iteration = 0 290 | self.min_pm = 0.0 291 | cdef mod2entry *e 292 | cdef int cn, vn 293 | 294 | for cn in range(self.m): 295 | self.current_cn_degree[cn] = self.cn_degree[cn] # restore CN degree for peeling 296 | for cn in range(self.m): 297 | self.current_cn[cn] = self.synd[cn] # all CN active, copy of syndrome 298 | for vn in range(self.n): 299 | self.current_vn[vn] = -1 # all VN active 300 | for vn in range(self.n): 301 | self.bp_decoding[vn] = 0 302 | 303 | return 304 | 305 | 306 | cdef int peel(self): 307 | # use activation info in self.current_vn and self.current_cn 308 | # to do peeling decoding, until no more VN can be decided 309 | # i.e., all CNs have degree >= 2 310 | cdef mod2entry *e 311 | cdef int cn, vn 312 | cdef bint degree_check 313 | while True: 314 | degree_check = True 315 | for cn in range(self.m): 316 | if self.current_cn[cn] == -1: # already cleared, therefore inactivated 317 | continue 318 | if self.current_cn_degree[cn] >= 2: # cannot decide any neighboring VN of this CN 319 | continue 320 | # must be degree 1, find the unique neighboring VN 321 | if self.current_cn_degree[cn] != 1: # sanity check. TODO: comment out 322 | print("in peel, expect cn", cn, "to have degree 1, but has degree", self.current_cn_degree[cn]) 323 | degree_check = False # still need to check all CNs in next loop 324 | vn = -1 325 | # iterate through VNs checked by this CN 326 | e = mod2sparse_first_in_row(self.H, cn) 327 | while not mod2sparse_at_end(e): 328 | if self.current_vn[e.col] != -1: # inactive VN 329 | e = mod2sparse_next_in_row(e) 330 | continue 331 | vn = e.col # found this unique VN 332 | break 333 | if self.vn_set_value(vn, self.current_cn[cn]) == -1: 334 | return -1 335 | 336 | if degree_check: # all CNs have degree >= 2, peeling ends 337 | return 0 338 | return 0 339 | 340 | cdef int vn_set_value(self, vn, value): 341 | # peel one VN 342 | if self.current_vn[vn] != -1: 343 | print("vn", vn, "already decided with value", self.current_vn[vn], "but is set again with value", value) 344 | if self.current_vn[vn] == value: 345 | return 0 346 | else: 347 | return -1 348 | self.current_vn[vn] = value 349 | self.bp_decoding[vn] = value 350 | cdef mod2entry* e 351 | cdef int cn, deg 352 | # iterate through all the neighboring CNs 353 | e = mod2sparse_first_in_col(self.H, vn) 354 | while not mod2sparse_at_end(e): 355 | if self.current_cn[e.row] == -1: # inactivate CN 356 | e = mod2sparse_next_in_col(e) 357 | continue 358 | cn = e.row 359 | deg = self.current_cn_degree[cn] - 1 360 | if value: # change CN node value based on the VN decision value 361 | self.current_cn[cn] = 1 - self.current_cn[cn] # 0->1, 1->0 362 | if deg == 0: # this check is cleared, inactivate this check 363 | if self.current_cn[cn] != 0: # contradiction 364 | return -1 365 | self.current_cn[cn] = -1 366 | self.current_cn_degree[cn] = deg 367 | e = mod2sparse_next_in_col(e) 368 | return 0 369 | 370 | cdef void bp_init(self): 371 | # initialisation 372 | for vn in range(self.n): 373 | if self.current_vn[vn] != -1: 374 | continue 375 | e = mod2sparse_first_in_col(self.H, vn) 376 | llr = self.channel_llr[vn] 377 | while not mod2sparse_at_end(e): 378 | e.bit_to_check = llr 379 | e = mod2sparse_next_in_col(e) 380 | 381 | cdef int bp_decode_llr(self, max_iter): 382 | 383 | cdef mod2entry *e 384 | cdef int cn, vn, iteration, sgn 385 | cdef bint equal 386 | cdef double temp, alpha 387 | 388 | self.converge = 0 389 | for iteration in range(max_iter): 390 | self.bp_iteration += 1 391 | #min-sum check to bit messages 392 | alpha = self.ms_scaling_factor 393 | for cn in range(self.m): 394 | if self.current_cn[cn] == -1: # inactivate CN 395 | continue 396 | # iterate through all the activate neighboring VNs 397 | e = mod2sparse_first_in_row(self.H, cn) 398 | temp = 1e308 399 | 400 | if self.current_cn[cn] == 1: sgn = 1 # use current_cn instead of self.synd 401 | else: sgn = 0 402 | # first pass, find the min abs value of all incoming messages, determine sign 403 | while not mod2sparse_at_end(e): 404 | if self.current_vn[e.col] != -1: 405 | e = mod2sparse_next_in_row(e) 406 | continue 407 | 408 | e.check_to_bit = temp # store min from the left most to itself (not inclusive) 409 | e.sgn = sgn 410 | 411 | # clipping 412 | if e.bit_to_check > 50.0: e.bit_to_check = 50.0 413 | elif e.bit_to_check < -50.0: e.bit_to_check = -50.0 414 | 415 | if abs(e.bit_to_check) < temp: 416 | temp = abs(e.bit_to_check) 417 | if e.bit_to_check <= 0: sgn = 1 - sgn 418 | e = mod2sparse_next_in_row(e) 419 | 420 | # second pass, set min to second min, others to min 421 | e = mod2sparse_last_in_row(self.H, cn) 422 | temp = 1e308 423 | sgn = 0 424 | while not mod2sparse_at_end(e): 425 | if self.current_vn[e.col] != -1: 426 | e = mod2sparse_prev_in_row(e) 427 | continue 428 | 429 | if temp < e.check_to_bit: 430 | e.check_to_bit = temp 431 | e.sgn += sgn 432 | 433 | e.check_to_bit *= ((-1)**e.sgn) * alpha 434 | 435 | if abs(e.bit_to_check) < temp: # store the min from the right most to itself 436 | temp = abs(e.bit_to_check) 437 | if e.bit_to_check <= 0: sgn = 1 - sgn 438 | 439 | e = mod2sparse_prev_in_row(e) 440 | 441 | # bit-to-check messages 442 | for vn in range(self.n): 443 | if self.current_vn[vn] != -1: 444 | continue 445 | 446 | e = mod2sparse_first_in_col(self.H, vn) 447 | temp = self.channel_llr[vn] 448 | 449 | while not mod2sparse_at_end(e): 450 | if self.current_cn[e.row] == -1: 451 | e = mod2sparse_next_in_col(e) 452 | continue 453 | 454 | e.bit_to_check = temp # sum from the left to itself 455 | temp += e.check_to_bit 456 | e = mod2sparse_next_in_col(e) 457 | 458 | self.log_prob_ratios[vn][iteration % self.history_length] = temp 459 | if temp <= 0: self.bp_decoding[vn] = 1 460 | else: self.bp_decoding[vn] = 0 461 | 462 | e = mod2sparse_last_in_col(self.H, vn) 463 | temp = 0.0 464 | while not mod2sparse_at_end(e): 465 | if self.current_cn[e.row] == -1: 466 | e = mod2sparse_prev_in_col(e) 467 | continue 468 | 469 | e.bit_to_check += temp # plus the sum from the right to itself 470 | temp += e.check_to_bit 471 | e = mod2sparse_prev_in_col(e) 472 | 473 | # check if converged 474 | mod2sparse_mulvec_cpp(self.H, self.bp_decoding, self.bp_decoding_synd) 475 | 476 | equal = True 477 | for cn in range(self.m): 478 | if self.synd[cn] != self.bp_decoding_synd[cn]: 479 | equal = False 480 | break 481 | if equal: 482 | self.converge = 1 483 | return 1 484 | 485 | return 0 486 | 487 | @property 488 | def bp_iteration(self): 489 | return self.bp_iteration 490 | 491 | @property 492 | def converge(self): 493 | return self.converge 494 | 495 | @property 496 | def min_pm(self): 497 | return self.min_pm 498 | 499 | @property 500 | def bp_decoding(self): 501 | return char2numpy(self.bp_decoding, self.n) 502 | 503 | @property 504 | def osdw_decoding(self): 505 | return char2numpy(self.osdw_decoding,self.n) 506 | 507 | @property 508 | def osd0_decoding(self): 509 | return char2numpy(self.osd0_decoding,self.n) 510 | 511 | @property 512 | def log_prob_ratios(self): 513 | cdef np.ndarray[np.float_t, ndim=2] np_array = np.zeros((self.n, self.history_length)) 514 | for i in range(self.n): 515 | for j in range(self.history_length): 516 | np_array[i,j] = self.log_prob_ratios[i][j] 517 | return np_array 518 | 519 | def __dealloc__(self): 520 | if self.MEM_ALLOCATED: 521 | free(self.synd) 522 | free(self.bp_decoding_synd) 523 | free(self.bp_decoding) 524 | free(self.channel_llr) 525 | for i in range(self.n): 526 | free(self.log_prob_ratios[i]) 527 | free(self.log_prob_ratios) 528 | 529 | free(self.current_vn) 530 | free(self.current_cn) 531 | free(self.cn_degree) 532 | free(self.current_cn_degree) 533 | free(self.cols) 534 | 535 | # OSD 536 | free(self.osd0_decoding) 537 | free(self.osdw_decoding) 538 | if self.osd_order>-1: 539 | free(self.rows) 540 | free(self.orig_cols) 541 | if self.osd_order>0: 542 | free(self.Htx) 543 | free(self.g) 544 | free(self.y) 545 | free(self.Ht_cols) 546 | if self.encoding_input_count!=0: 547 | for i in range(self.encoding_input_count): 548 | free(self.osdw_encoding_inputs[i]) 549 | 550 | mod2sparse_free_cpp(self.H) -------------------------------------------------------------------------------- /src/simulation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from ldpc import BpOsdDecoder 4 | import time 5 | from src.utils import rank 6 | from src.codes_q import * 7 | from src import bpgdg_decoder 8 | from src import bp4_osd 9 | 10 | def data_qubit_noise_decoding(code, p, num_shots=1000, osd_orders=[10], osd_factor=0.625, skip_gdg=False, 11 | max_step=40, max_tree_step=30, max_iter_per_step=6, 12 | extra_decoders=[]): 13 | print(f"hx shape {code.hx.shape}, hz_perp shape {code.hz_perp.shape}") 14 | # print(f"girth hx {find_girth(code.hx)}, hz {find_girth(code.hz)}") # a bit slow for the 882 code 15 | err = np.random.binomial(1, p, (num_shots, code.N)) # [num_shots, N] 16 | syndrome = (err @ code.hx.T) % 2 # [num_shots, N_half] 17 | priors = np.ones(code.N) * p 18 | for dec in extra_decoders: 19 | start_time = time.perf_counter() 20 | num_err = 0 21 | num_flag_err = 0 22 | for i in range(num_shots): 23 | s = syndrome[i] 24 | e_hat = dec.decode(s) 25 | e_diff = (e_hat + err[i]) % 2 26 | logical_err = ((e_diff @ code.hz_perp.T) % 2).any() 27 | num_err += logical_err 28 | num_flag_err += 1 - dec.converge 29 | print("Extra decoder: num flagged error", num_flag_err) 30 | print(f"Extra decoder: num logical error {num_err}/{num_shots}, LER {num_err/num_shots}") 31 | end_time = time.perf_counter() 32 | print("Elapsed time:", end_time-start_time) 33 | 34 | # OSD 35 | start_time = time.perf_counter() 36 | for order in osd_orders: 37 | osd_num_err = 0 38 | osd0_num_err = 0 39 | bpd = BpOsdDecoder( 40 | code.hx, 41 | channel_probs=list(priors), 42 | max_iter=100, 43 | bp_method="minimum_sum", 44 | ms_scaling_factor=osd_factor, # usually {0.5, 0.625, 0.8, 1.0} suffice 45 | osd_method="OSD_CS", 46 | osd_order=order, # use -1 for BP alone 47 | ) 48 | for i in range(num_shots): 49 | s = syndrome[i] 50 | e_hat_osd = bpd.decode(s) # can extract osd_0 result via bpd.osd0_decoding when using higher order osd 51 | e_diff = (e_hat_osd + err[i]) % 2 52 | logical_err = ((e_diff @ code.hz_perp.T) % 2).any() 53 | osd_num_err += logical_err 54 | e_diff = (bpd.osd0_decoding + err[i]) % 2 55 | logical_err = ((e_diff @ code.hz_perp.T) % 2).any() 56 | osd0_num_err += logical_err 57 | 58 | print(f"OSD order 0: num logical error {osd0_num_err}/{num_shots}, LER {osd0_num_err/num_shots}") 59 | print(f"OSD order {order}: num logical error {osd_num_err}/{num_shots}, LER {osd_num_err/num_shots}") 60 | 61 | end_time = time.perf_counter() 62 | print("Elapsed time:", end_time-start_time) 63 | if skip_gdg: return 64 | 65 | # GDG 66 | bpgdg = bpgdg_decoder( 67 | code.hx, 68 | channel_probs=priors, 69 | max_iter_per_step=max_iter_per_step, 70 | gdg_factor=0.625, 71 | max_step=max_step, # have to use larger max_step for longer block-length codes 72 | max_tree_depth=4, 73 | max_side_depth=20, 74 | max_tree_branch_step=max_tree_step, 75 | max_side_branch_step=max_step-20, 76 | multi_thread=True, 77 | low_error_mode=True, # always use low error mode 78 | # don't care about the rest 79 | max_iter=24, 80 | ms_scaling_factor=0.625, 81 | new_n=code.N 82 | ) 83 | 84 | gdg_num_err = 0 85 | num_flag_err = 0 86 | start_time = time.perf_counter() 87 | for i in range(num_shots): 88 | s = syndrome[i] 89 | e_hat_gdg = bpgdg.decode(s) 90 | num_flag_err += 1 - bpgdg.converge 91 | e_diff = (e_hat_gdg + err[i]) % 2 92 | logical_err = ((e_diff @ code.hz_perp.T) % 2).any() 93 | gdg_num_err += logical_err 94 | 95 | 96 | print("GDG: num flagged error", num_flag_err) 97 | print(f"GDG: num logical error {gdg_num_err}/{num_shots}, LER {gdg_num_err/num_shots}") 98 | end_time = time.perf_counter() 99 | print("Elapsed time:", end_time-start_time) -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-FileCopyrightText: Copyright (c) 2021-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Utility functions and layers for the FEC package.""" 6 | 7 | import numpy as np 8 | from collections import deque 9 | 10 | def bin2int(arr): 11 | """Convert binary array to integer. 12 | 13 | For example ``arr`` = `[1, 0, 1]` is converted to `5`. 14 | 15 | Input 16 | ----- 17 | arr: int or float 18 | An iterable that yields 0's and 1's. 19 | 20 | Output 21 | ----- 22 | : int 23 | Integer representation of ``arr``. 24 | 25 | """ 26 | if len(arr) == 0: return None 27 | return int(''.join([str(x) for x in arr]), 2) 28 | 29 | def int2bin(num, len_): 30 | """ 31 | Convert ``num`` of int type to list of length ``len_`` with 0's and 1's. 32 | ``num`` and ``len_`` have to non-negative. 33 | 34 | For e.g., ``num`` = `5`; `int2bin(num`, ``len_`` =4) = `[0, 1, 0, 1]`. 35 | 36 | For e.g., ``num`` = `12`; `int2bin(num`, ``len_`` =3) = `[1, 0, 0]`. 37 | 38 | Input 39 | ----- 40 | num: int 41 | An integer to be converted into binary representation. 42 | 43 | len_: int 44 | An integer defining the length of the desired output. 45 | 46 | Output 47 | ----- 48 | : list of int 49 | Binary representation of ``num`` of length ``len_``. 50 | """ 51 | assert num >= 0, "Input integer should be non-negative" 52 | assert len_ >= 0, "width should be non-negative" 53 | 54 | bin_ = format(num, f'0{len_}b') 55 | binary_vals = [int(x) for x in bin_[-len_:]] if len_ else [] 56 | return binary_vals 57 | 58 | def alist2mat(alist, verbose=True): 59 | # pylint: disable=line-too-long 60 | r"""Convert `alist` [MacKay]_ code definition to `full` parity-check matrix. 61 | 62 | Many code examples can be found in [UniKL]_. 63 | 64 | About `alist` (see [MacKay]_ for details): 65 | 66 | - `1.` Row defines parity-check matrix dimension `m x n` 67 | - `2.` Row defines int with `max_CN_degree`, `max_VN_degree` 68 | - `3.` Row defines VN degree of all `n` column 69 | - `4.` Row defines CN degree of all `m` rows 70 | - Next `n` rows contain non-zero entries of each column (can be zero padded at the end) 71 | - Next `m` rows contain non-zero entries of each row. 72 | 73 | Input 74 | ----- 75 | alist: list 76 | Nested list in `alist`-format [MacKay]_. 77 | 78 | verbose: bool 79 | Defaults to True. If True, the code parameters are printed. 80 | 81 | Output 82 | ------ 83 | (pcm, k, n, coderate): 84 | Tuple: 85 | 86 | pcm: ndarray 87 | NumPy array of shape `[n-k, n]` containing the parity-check matrix. 88 | 89 | k: int 90 | Number of information bits. 91 | 92 | n: int 93 | Number of codewords bits. 94 | 95 | coderate: float 96 | Coderate of the code. 97 | 98 | Note 99 | ---- 100 | Use :class:`~sionna.fec.utils.load_alist` to import alist from a 101 | textfile. 102 | 103 | For example, the following code snippet will import an alist from a file called ``filename``: 104 | 105 | .. code-block:: python 106 | 107 | al = load_alist(path = filename) 108 | pcm, k, n, coderate = alist2mat(al) 109 | """ 110 | 111 | assert len(alist)>4, "Invalid alist format." 112 | 113 | n = alist[0][0] 114 | m = alist[0][1] 115 | v_max = alist[1][0] 116 | c_max = alist[1][1] 117 | k = n - m 118 | coderate = k / n 119 | 120 | vn_profile = alist[2] 121 | cn_profile = alist[3] 122 | 123 | # plausibility checks 124 | assert np.sum(vn_profile)==np.sum(cn_profile), "Invalid alist format." 125 | assert np.max(vn_profile)==v_max, "Invalid alist format." 126 | assert np.max(cn_profile)==c_max, "Invalid alist format." 127 | 128 | if len(alist)==len(vn_profile)+4: 129 | print("Note: .alist does not contain (redundant) CN perspective.") 130 | print("Recovering parity-check matrix from VN only.") 131 | print("Please verify the correctness of the results manually.") 132 | vn_only = True 133 | else: 134 | assert len(alist)==len(vn_profile) + len(cn_profile) + 4, \ 135 | "Invalid alist format." 136 | vn_only = False 137 | 138 | pcm = np.zeros((m,n)) 139 | num_edges = 0 # count number of edges 140 | 141 | for idx_v in range(n): 142 | for idx_i in range(vn_profile[idx_v]): 143 | # first 4 rows of alist contain meta information 144 | idx_c = alist[4+idx_v][idx_i]-1 # "-1" as this is python 145 | pcm[idx_c, idx_v] = 1 146 | num_edges += 1 # count number of edges (=each non-zero entry) 147 | 148 | # validate results from CN perspective 149 | if not vn_only: 150 | for idx_c in range(m): 151 | for idx_i in range(cn_profile[idx_c]): 152 | # first 4 rows of alist contain meta information 153 | # follwing n rows contained VN perspective 154 | idx_v = alist[4+n+idx_c][idx_i]-1 # "-1" as this is python 155 | assert pcm[idx_c, idx_v]==1 # entry must already exist 156 | 157 | if verbose: 158 | print("Number of variable nodes (columns): ", n) 159 | print("Number of check nodes (rows): ", m) 160 | print("Number of information bits per cw: ", k) 161 | print("Number edges: ", num_edges) 162 | print("Max. VN degree: ", v_max) 163 | print("Max. CN degree: ", c_max) 164 | print("VN degree: ", vn_profile) 165 | print("CN degree: ", cn_profile) 166 | 167 | return pcm, k, n, coderate 168 | 169 | def load_alist(path): 170 | """Read `alist`-file [MacKay]_ and return nested list describing the 171 | parity-check matrix of a code. 172 | 173 | Many code examples can be found in [UniKL]_. 174 | 175 | Input 176 | ----- 177 | path:str 178 | Path to file to be loaded. 179 | 180 | Output 181 | ------ 182 | alist: list 183 | A nested list containing the imported alist data. 184 | """ 185 | 186 | alist = [] 187 | with open(path, "r") as reader: # pylint: disable=unspecified-encoding 188 | # read list line by line (different length) 189 | for line in reader: 190 | l = [] 191 | # append all entries 192 | for word in line.split(): 193 | l.append(int(word)) 194 | if l: # ignore empty lines 195 | alist.append(l) 196 | 197 | return alist 198 | 199 | def make_systematic(mat, is_pcm=False): 200 | r"""Bring binary matrix in its systematic form. 201 | 202 | Input 203 | ----- 204 | mat : ndarray 205 | Binary matrix to be transformed to systematic form of shape `[k, n]`. 206 | 207 | is_pcm: bool 208 | Defaults to False. If true, ``mat`` is interpreted as parity-check 209 | matrix and, thus, the last k columns will be the identity part. 210 | 211 | Output 212 | ------ 213 | mat_sys: ndarray 214 | Binary matrix in systematic form, i.e., the first `k` columns equal the 215 | identity matrix (or last `k` if ``is_pcm`` is True). 216 | 217 | column_swaps: list of int tuples 218 | A list of integer tuples that describes the swapped columns (in the 219 | order of execution). 220 | 221 | Note 222 | ---- 223 | This algorithm (potentially) swaps columns of the input matrix. Thus, the 224 | resulting systematic matrix (potentially) relates to a permuted version of 225 | the code, this is defined by the returned list ``column_swap``. 226 | Note that, the inverse permutation must be applied in the inverse list 227 | order (in case specific columns are swapped multiple times). 228 | 229 | If a parity-check matrix is passed as input (i.e., ``is_pcm`` is True), the 230 | identity part will be re-arranged to the last columns.""" 231 | 232 | m = mat.shape[0] 233 | n = mat.shape[1] 234 | 235 | assert m<=n, "Invalid matrix dimensions." 236 | 237 | # check for all-zero columns (=unchecked nodes) 238 | if is_pcm: 239 | c_node_deg = np.sum(mat, axis=0) 240 | if np.any(c_node_deg==0): 241 | warnings.warn("All-zero column in parity-check matrix detected. " \ 242 | "It seems as if the code contains unprotected nodes.") 243 | 244 | mat = np.copy(mat) 245 | column_swaps = [] # store all column swaps 246 | 247 | # convert to bool for faster arithmetics 248 | mat = mat.astype(bool) 249 | 250 | # bring in upper triangular form 251 | for idx_c in range(m): 252 | success = False 253 | # step 1: find next leading "1" 254 | for idx_r in range(idx_c,m): 255 | # skip if entry is "0" 256 | if mat[idx_r, idx_c]: 257 | mat[[idx_c, idx_r]] = mat[[idx_r, idx_c]] # swap rows 258 | success = True 259 | break 260 | 261 | # Could not find "1"-entry for column idx_c 262 | # => swap with columns from non-sys part 263 | # The task is to find a column with index idx_cc that has a "1" at 264 | # row idx_c 265 | if not success: 266 | for idx_cc in range(m, n): 267 | if mat[idx_c, idx_cc]: 268 | # swap columns 269 | mat[:,[idx_c, idx_cc]] = mat[:,[idx_cc, idx_c]] 270 | column_swaps.append([idx_c, idx_cc]) 271 | success=True 272 | break 273 | 274 | if not success: 275 | raise ValueError("Could not succeed; mat is not full rank?") 276 | 277 | # we can now assume a leading "1" at row idx_c 278 | for idx_r in range(idx_c+1, m): 279 | if mat[idx_r, idx_c]: 280 | mat[idx_r,:] ^= mat[idx_c,:] # bin. add of row idx_c to idx_r 281 | 282 | # remove upper triangle part in inverse order 283 | for idx_c in range(m-1, -1, -1): 284 | for idx_r in range(idx_c-1, -1, -1): 285 | if mat[idx_r, idx_c]: 286 | mat[idx_r,:] ^= mat[idx_c,:] # bin. add of row idx_c to idx_r 287 | 288 | # verify results 289 | assert np.array_equal(mat[:,:m], np.eye(m)), \ 290 | "Internal error, could not find systematic matrix." 291 | 292 | # bring identity part to end of matrix if parity-check matrix is provided 293 | if is_pcm: 294 | im = np.copy(mat[:,:m]) 295 | mat[:,:m] = mat[:,-m:] 296 | mat[:,-m:] = im 297 | # and track column swaps 298 | for idx in range(m): 299 | column_swaps.append([idx, n-m+idx]) 300 | 301 | # return integer array 302 | mat = mat.astype(int) 303 | return mat, column_swaps 304 | 305 | ########################################################### 306 | # Functions adapted from the ldpc package 307 | ########################################################### 308 | 309 | def row_echelon(mat, reduced=False): 310 | r"""Converts a binary matrix to (reduced) row echelon form via Gaussian Elimination, 311 | also works for rank-deficient matrix. Unlike the make_systematic method, 312 | no column swaps will be performed. 313 | 314 | Input 315 | ---------- 316 | mat : ndarry 317 | A binary matrix in numpy.ndarray format. 318 | reduced: bool 319 | Defaults to False. If true, the reduced row echelon form is returned. 320 | 321 | Output 322 | ------- 323 | row_ech_form: ndarray 324 | The row echelon form of input matrix. 325 | rank: int 326 | The rank of the matrix. 327 | transform: ndarray 328 | The transformation matrix such that (transform_matrix@matrix)=row_ech_form 329 | pivot_cols: list 330 | List of the indices of pivot num_cols found during Gaussian elimination 331 | """ 332 | 333 | m, n = np.shape(mat) 334 | # Don't do "m<=n" check, allow over-complete matrices 335 | mat = np.copy(mat) 336 | # Convert to bool for faster arithmetics 337 | mat = mat.astype(bool) 338 | transform = np.identity(m).astype(bool) 339 | pivot_row = 0 340 | pivot_cols = [] 341 | 342 | # Allow all-zero column. Row operations won't induce all-zero columns, if they are not present originally. 343 | # The make_systematic method will swap all-zero columns with later non-all-zero columns. 344 | # Iterate over cols, for each col find a pivot (if it exists) 345 | for col in range(n): 346 | # Select the pivot - if not in this row, swap rows to bring a 1 to this row, if possible 347 | if not mat[pivot_row, col]: 348 | # Find a row with a 1 in this column 349 | swap_row_index = pivot_row + np.argmax(mat[pivot_row:m, col]) 350 | # If an appropriate row is found, swap it with the pivot. Otherwise, all zeroes - will loop to next col 351 | if mat[swap_row_index, col]: 352 | # Swap rows 353 | mat[[swap_row_index, pivot_row]] = mat[[pivot_row, swap_row_index]] 354 | # Transformation matrix update to reflect this row swap 355 | transform[[swap_row_index, pivot_row]] = transform[[pivot_row, swap_row_index]] 356 | 357 | if mat[pivot_row, col]: # will evaluate to True if this column is not all-zero 358 | if not reduced: # clean entries below the pivot 359 | elimination_range = [k for k in range(pivot_row + 1, m)] 360 | else: # clean entries above and below the pivot 361 | elimination_range = [k for k in range(m) if k != pivot_row] 362 | for idx_r in elimination_range: 363 | if mat[idx_r, col]: 364 | mat[idx_r] ^= mat[pivot_row] 365 | transform[idx_r] ^= transform[pivot_row] 366 | pivot_row += 1 367 | pivot_cols.append(col) 368 | 369 | if pivot_row >= m: # no more rows to search 370 | break 371 | 372 | rank = pivot_row 373 | row_ech_form = mat.astype(int) 374 | 375 | return [row_ech_form, rank, transform.astype(int), pivot_cols] 376 | 377 | def rank(mat): 378 | r"""Returns the rank of a binary matrix 379 | 380 | Input 381 | ---------- 382 | mat: ndarray 383 | A binary matrix in numpy.ndarray format 384 | 385 | Output 386 | ------- 387 | int 388 | The rank of the matrix""" 389 | return row_echelon(mat)[1] 390 | 391 | def kernel(mat): 392 | r"""Computes the kernel of the matrix M. 393 | All vectors x in the kernel of M satisfy the following condition:: 394 | 395 | Mx=0 \forall x \in ker(M) 396 | 397 | Input 398 | ---------- 399 | mat: ndarray 400 | A binary matrix in numpy.ndarray format. 401 | 402 | Output 403 | ------- 404 | ker: ndarray 405 | A binary matrix which is the kernel of the input binary matrix. 406 | 407 | rank: int 408 | Rank of transposed mat, which is the same as the rank of mat. 409 | 410 | pivot_cols: list 411 | List of the indices of pivot of the transposed mat. Can be used in row_basis. 412 | 413 | Note 414 | ----- 415 | Why does this work? 416 | 417 | The transformation matrix, P, transforms the matrix M into row echelon form, ReM:: 418 | 419 | P@M=ReM=[A,0]^T, 420 | 421 | where the width of A is equal to the rank. This means the bottom n-k rows of P 422 | must produce a zero vector when applied to M. For a more formal definition see 423 | the Rank-Nullity theorem. 424 | """ 425 | 426 | transpose = mat.T 427 | m, _ = transpose.shape 428 | _, rank, transform, pivot_cols = row_echelon(transpose) 429 | ker = transform[rank:m] 430 | return ker, rank, pivot_cols 431 | 432 | def row_basis(mat): 433 | r"""Outputs a basis for the rows of the matrix. 434 | 435 | Input 436 | ---------- 437 | mat: ndarray 438 | The input matrix. 439 | 440 | Output 441 | ------- 442 | basis: ndarray 443 | A numpy.ndarray matrix where each row is a basis element.""" 444 | return mat[row_echelon(mat.T)[3]] 445 | 446 | def compute_code_distance(mat, is_pcm=True, is_basis=False): 447 | r'''Computes the distance of the linear code given by the input parity check / generator matrix. 448 | The code distance is given by the minimum weight of a nonzero codeword. 449 | 450 | Note 451 | ---- 452 | The runtime of this function scales exponentially with the block size. In practice, computing the code distance of codes with block lengths greater than ~10 will be very slow. 453 | 454 | Parameters 455 | ---------- 456 | mat: ndarray 457 | The parity check matrix 458 | 459 | is_pcm: bool 460 | Defaults to True. If false, mat is interpreted as a generator matrix. 461 | 462 | Returns 463 | ------- 464 | int 465 | The code distance 466 | ''' 467 | gen = mat 468 | if is_pcm: 469 | gen = kernel(mat) 470 | if len(gen)==0: return np.inf # infinite code distance 471 | cw = gen 472 | if not is_basis: 473 | cw = row_basis(gen) # nonzero codewords 474 | return np.min(np.sum(cw, axis=1)) 475 | 476 | def inverse(mat): 477 | r"""Computes the left inverse of a full-rank matrix. 478 | 479 | Input 480 | ---------- 481 | matrix: ndarray 482 | The binary matrix to be inverted in numpy.ndarray format. This matrix must either be 483 | square full-rank or rectangular with full-column rank. 484 | 485 | Output 486 | ------- 487 | inverse: ndarray 488 | The inverted binary matrix 489 | 490 | Note 491 | ----- 492 | The `left inverse' is computed when the number of rows in the matrix 493 | exceeds the matrix rank. The left inverse is defined as follows:: 494 | 495 | Inverse(M.T@M)@M.T 496 | 497 | We can make a further simplification by noting that the row echelon form matrix 498 | with full column rank has the form:: 499 | 500 | row_echelon_form=P@M=vstack[I,A] 501 | 502 | In this case the left inverse simplifies to:: 503 | 504 | Inverse(M^T@P^T@P@M)@M^T@P^T@P=M^T@P^T@P=row_echelon_form.T@P""" 505 | 506 | m, n = mat.shape 507 | reduced_row_ech, rank, transform, _ = row_echelon(mat, reduced=True) 508 | if m == n and rank == m: 509 | return transform 510 | # compute the left-inverse 511 | elif m > rank and n == rank: # left inverse 512 | return reduced_row_ech.T @ transform % 2 513 | else: 514 | raise ValueError("This matrix is not invertible. Please provide either a full-rank square\ 515 | matrix or a rectangular matrix with full column rank.") 516 | 517 | def hopcroft_karp(adj, U, V): 518 | r"""Hopcroft-Karp maximum matching for bipartite graphs. 519 | 520 | Input 521 | --------- 522 | adj (dict): adjacency list from U to list of neighbors in V 523 | U (iterable): left vertex set 524 | V (iterable): right vertex set 525 | 526 | Output 527 | --------- 528 | dict: matching as a map u->v for matched pairs 529 | """ 530 | INF = float('inf') 531 | pair_U = {u: None for u in U} 532 | pair_V = {v: None for v in V} 533 | dist = {} 534 | 535 | def bfs(): 536 | queue = deque() 537 | for u in U: 538 | if pair_U[u] is None: 539 | dist[u] = 0 540 | queue.append(u) 541 | else: 542 | dist[u] = INF 543 | dist[None] = INF 544 | 545 | while queue: 546 | u = queue.popleft() 547 | if dist[u] < dist[None]: 548 | for v in adj.get(u, []): 549 | pu = pair_V[v] 550 | if pu is None: 551 | dist[None] = dist[u] + 1 552 | elif dist[pu] == INF: 553 | dist[pu] = dist[u] + 1 554 | queue.append(pu) 555 | return dist[None] != INF 556 | 557 | def dfs(u): 558 | if u is not None: 559 | for v in adj.get(u, []): 560 | pu = pair_V[v] 561 | if pu is None or (dist[pu] == dist[u] + 1 and dfs(pu)): 562 | pair_U[u] = v 563 | pair_V[v] = u 564 | return True 565 | dist[u] = INF 566 | return False 567 | return True 568 | 569 | matching = 0 570 | while bfs(): 571 | for u in U: 572 | if pair_U[u] is None and dfs(u): 573 | matching += 1 574 | return {u: pair_U[u] for u in U if pair_U[u] is not None} 575 | 576 | 577 | def edge_coloring_bipartite(adj_mat): 578 | r"""Edge-color a bipartite graph by iteratively extracting maximum matchings. 579 | 580 | Input 581 | -------- 582 | U (iterable): vertices in left partition 583 | V (iterable): vertices in right partition 584 | edges (list of tuple): list of (u,v) edges with u in U and v in V 585 | 586 | Output 587 | ------- 588 | dict: mapping (u,v) to color index (1..num_colors) 589 | int: the number of colors used (>= max degree) 590 | 591 | Note 592 | -------- 593 | This algorithm might use more than Δ+1 colors 594 | 595 | """ 596 | # Build adjacency list from adjacency matrix 597 | num_row, num_col = adj_mat.shape 598 | U, V = list(range(num_row)), list(range(num_col)) 599 | # Build adjacency list copy 600 | adj = {u: [] for u in U} 601 | for u, v in zip(*adj_mat.nonzero()): 602 | adj[u].append(v) 603 | 604 | # Compute maximum degree Δ 605 | Delta = max(np.max(adj_mat.sum(axis=0)), np.max(adj_mat.sum(axis=1))) 606 | 607 | # Coloring by repeated matchings 608 | color = {} 609 | current_adj = {u: list(neighs) for u, neighs in adj.items()} 610 | num_colors = 0 611 | color_dict = {} 612 | for i in range(Delta): 613 | color_dict[i] = [] 614 | while any(current_adj[u] for u in U): 615 | num_colors += 1 616 | # find a maximum matching 617 | M = hopcroft_karp(current_adj, U, V) 618 | # assign this matching the new color 619 | for u, v in M.items(): 620 | color[(u, v)] = num_colors 621 | color_dict[num_colors-1].append((u,v)) 622 | current_adj[u].remove(v) # remove colored edge 623 | return color_dict, num_colors --------------------------------------------------------------------------------