A package for developing scalable and hardware-aware quantum error inference and compilation to minimise bitflip gate and measurement error.
Project description
UnNoiseCompile
A repository for developing scalable and hardware-aware quantum compilation to minimise bitflip gate and measurement noise.
Folder Structure
Much of this repository is built on proper management of experimental data gathered from probe circuits to be used on QPUs. As such, folder management and organistaion is crucial. The following is strongly suggested to ensure that the functions run smoothly without too much manipulation of subfolders:
Project_Root/
│
│
├── pyproject.toml <-- describing the version, dependencies, etc.
│
│
├── notebooks/
│ ├── notebook1.ipynb
│ └── notebook2.ipynb <-- Any notebooks to run scripts
│
├── src/
│ ├── __init__.py
│ ├── graphconnector.py <-- Build Graphs for 2-Qubit Gates
│ ├── splitmeasfilter.py <-- Measurement Error & Inference
│ └── splitgatefilter.py <-- Gate Error & Inference
│
├── data/
│ ├── *meas_cal*/ <-- Output folder for MeasFilter
│ └── *gate_exp*/ <-- Output folder for GateFilter
│
└── results/ <-- Final plots and reports
Getting Started
Python modules built using Python 3.11.14 in /src, with notebooks and samples written mostly using Jupyter notebooks in /notebooks and /scripts/.
First, to make a .venv file as a virtual environment (use Python3.11 for Qiskit compatibility):
python3.11 -m venv .venv
Activate:
Linux/macOS:
source .venv/bin/activate
Windows Command Prompt:
.venv\Scripts\activate.bat
Then install packages using pip:
pip install -r requirements.txt
To allow access to the src/ folder for scripts, run the following to allow python to recognise the __init__.py and setup.py files already present:
pip install -e .
And run from there!
The Workflow
1. Measurement Data
You can supply bitstring data, in the form of the QuantumTask.result().measurements available from the Amazon Braket API into the SplitMeasFilter by placing a csv file named State0.csv defining the a nontrivial groundstate measurement in the path defined by the file_address parameter. Similarly, one should place State1.csv, a csv file containing the bits recieved from a nontrivial flip.
The script is designed to run inference on data gathered from an expected |000...000> state and an expected |111...111> state individually. This is due to the fact that measurement errors will generally be asymmetric, according to the physics of the QPU system. |0> is often a ground state, and relaxes more often than exciting to the |1> state, for example. Thus, we run two rounds of inference to understand these asymmetric properties.
Here is a sample of how one is to use the SplitMeasFilter Python Class, designed to infer posterior distributions of these errors, and efficiently filter these errors from given quantum data.
from src.splitmeasfilter import SplitMeasFilter
# Define measurement order of qubits
# VERY important, to ensure that the order of the data recieved is the order that AWS produced
# More often than not for complex circuits, this will not be in ascending order!
# Find through running `QuantumTask.result().measured_qubits` in the Braket API
qubits = [0, 1, 6]
# Define whichever directory contains both `State0.csv` and `State1.csv` (or only one if that alone is given)
meas_path = './data/meas_cal/'
# Instantiate Data (from `State0.csv` and `State1.csv` files)
meas_filter = SplitMeasFilter(qubit_order=qubits, # Qubit order as above
file_address=meas_path, # Location of data as above
load_data=True, # (optional) Load posteriors from previous inference trials
home_dir='.' # (optional) Define the working directory
)
We can then run inference on this data:
# Applying the meas_filter.inference() function
# on batches of size shots_per_point
# The 'prep_state' variable can be '0' or '1', defining either
# |000...000> or |111...111> to infer either error in measuring |0> or |1>
meas_filter.inference(nPrior=40000, # Number of tested error rates
prior_sd=0.1, # The sd of the prior distribution
shots_per_point=1024,# Number of shots in each batch
seed=28, # Seed for the rejection sampling
prep_state='0') # Prepared state (for |0> or |1>)
meas_filter.qubit_order = [1,0]
meas_filter.inference(nPrior=40000, # Number of tested error rates
prior_sd=0.1, # The sd of the prior distribution
shots_per_point=1024,# Number of shots in each batch
seed=28, # Seed for the rejection sampling
prep_state='1') # Prepared state (for |0> or |1>)
This will produce files in a dedicated Posterior Data directory located in the directory specified by the file_address parameter:
State0_Post_Qubit0.csv,
State0_Post_Qubit1.csv,
State0_Post_Qubit6.csv,
State1_Post_Qubit1.csv,
State1_Post_Qubit0.csv,
Which are sampled arrays from posterior distribution of error rates for each qubit and each state we inferred on. We also recieve the full data in the form of an easy to load (and read) JSON file, both containing full distributions as above and summary statistics (mean, mode, and confidence interval). These are found both timestamped to retain copies of data, along with the latest run marked as:
Post_Full_Current.json
Post_MeanMode_Current.json
Alongside the data, the posterior distributions will be saved as plots in the same file_address directory, named as:
ErrorDist_Qubit{QUBIT_NUMBER}.pdf
For example, here is a plot that may have been produced by the above script:
which contain the posterior distributions of measurement errors, in the form of rejection-sampled error rates. To reproduce these plots, along with printing a full description of all distribution statistics, and optionally save, run:
meas_filter.error_distributions(plotting=True, save_plots=True)
And this will produce the error rate variables in the MeasFilter class. If the SplitMeasFilter object is already loaded in, one can access the full posteriors (for example for Qubit 0) without loading a .csv with the following nested dictionaries post_full and post:
Qubit0_error_0_array = meas_filter.post_full['Qubit0']['0']
Qubit0_error_1_array = meas_filter.post_full['Qubit0']['1']
And the summary statistics:
Qubit0_error_0_mean = meas_filter.post['Qubit0']['e0mean']
Qubit0_error_0_mode = meas_filter.post['Qubit0']['e0mode']
Qubit0_error_0_CI = meas_filter.post_full['Qubit0']['e0CI95']
2. Gate Data
2a. Considering a Testing Circuit
It is not a trivial problem to design an efficient circuit to test 2-qubit gates. First we discuss which 2-qubit gates we can test:
If the Controlled-Z gate is the native 2-qubit gate (as for most superconducting QPUs at the time of writing in January of 2026), there is no direction to the gate, so CZ[Q1-Q2] is the same as CZ[Q2-Q1], so we need only test every connection between qubits. Also, the CZ gate will act on basis states only by adding a total phase, so under our bitflip assumption it is relatively easy to isolate individual qubit error rates.
If the iSWAP gate is the native 2-qubit gate (for instance on Rigetti's Ankaa-3), then there is also no direction, and iSWAP[Q1-Q2]_ is the same as iSWAP[Q2-Q1]. However, the iSWAP gate works on basis states by flipping the value of each qubit it acts on, and adding a phase. This makes isolating individual qubit error rates intractable, especially if many gates must be tested with high measurement error rates (unfortunately both are the case as of January 2026 for testing Ankaa-3!).
Single qubit gates are relatively simple to test. One runs a verbatim test, and ensures that the number of gates matches the circuit, and inputs the gate type and the number of gates. All standard native single qubit gates (full rotation gates) are considered in the module, and the expected state is automatically calculated and inferred against
2b. Designing a 2-Qubit Testing Circuit
However, superconducting qubits are often arranged in somewhat complex arrangements on a cartesian grid, with a connectivity of any qubit being anywhere from 1 to 4. The arrangements can also change if the QPU has been calibrated and qubits removed or disconnected. We will then need to construct 4 circuits, where each 2-qubit connection is tested in at least one of these 4 circuits.
As this problem is equivalent to the maximal edge-colouring problem from graph theory, there are many algorithms which have been developed to efficiently generate solutions to this exact problem, 'colouring' each connection at least once, and if possible, multiple times, to design 4 circuits which will connect all qubits between them. Using graph theory, we can also say that since the qubits are arranged on a grid, it is a bipartite graph, and thus by König's Theorem it is class 1, and only 4 circuits (and not 5) are required (for this idea, see Vizing's Theorem).
The graphconnector.py script and its MisraGriesSolver class is an implementation using Kempe Chains in the Misra-Gries algorithm and graph reordering to produce valid data and maximise data quality and efficiency.
We use the Misra-Gries Edge-Colouring Algorithm, which is efficient of order $\mathcal{O}(N \times E) \equiv \mathcal{O}(N ^ 2)$, of order 'N' number of qubits multiply 'E' number of edges (here qubit coupling). This will be very efficient for NISQ QPUs, and can be improved if the number of circuits can be increased from 4 to 5, or shots become less expensive, wherein finding these graphs becomes easier.
We here minimise the number of circuits to 4 (which will be either 4 or 5 in a grid-like construction as in current superconducting QPUs), and add in redundant extra qubit calibration coupling tests to infer the gate error with higher accuracy with limited shots.
Below are 2-qubit CZ testing circuit examples from IQM Emerald and Rigetti Ankaa-3. Each have 4 circuits, with red edges being active CZ gates in the circuit, to be run some number of times to determine bit-flip error. An 'Efficiency' is given in the top left of each circuit, which gives the proportion of qubits active in a CZ gate, thus from whom error data can be gathered. Additional statistics concerning the graphs, total CZ gates tested, redundnacy, and time taken to produce them are given in the Ankaa-3 graphs:
IQM Emerald:
Note that the top left corners of each qubit connection graph displays the 'efficiency.' This refers to the number of qubits which are probed in that circuit (0.0 being none, 1.0 being all).
Rigetti Ankaa-3:
Note that the top left corners of each qubit connection graph displays the 'efficiency.' This refers to the number of qubits which are probed in that circuit (0.0 being none, 1.0 being all).
Rigetti Ankaa-3 Text Statistics Readout:
The notebook containing the code to produce these graphs from the AWS Braket on 7 Jan 2026 can be found in Tutorial/Producing2QubitGraphs.ipynb
2b. Inference
After gathering data from a circuit applying the tested gate to each qubit a set number of times, using the SplitGateFilter class, the gate error can be efficiently inferred. The data should be placed into the directory marked by the data_file_address parameter, with the files named as Readout_{gate_num}{gate_type}Q{QubitNumber}.csv.
from src.splitgatefilter import SplitGateFilter
# Instantiate our filter class
# Note: data_file_address is used for both I/O in the current script.
# Ensure raw gate bitstring data gathered from the AWS Braket
# in the same method as above in Section 1. is contained as a .csv inside 'gate_path'.
# It is recommended to directly provide the name here to ensure file access safety.
my_gate_filter = SplitGateFilter(home_dir='./',meas_cal_dir='./meas_cal/',data_file_address='./2-qubit-data/')
Note the three folders. home_dir is the project directory, it is optional to declare. data_file_address contains all the measured gate data, and will then contain the folder produced by SplitGateFilter where the final results are placed, all plots, all posterior distribution arrays, etc. meas_cal_dir contains the calibrated informative priors from the SplitMeasFilter class performed prior to this calculation.
It is highly recommended to run the SplitMeasFilter script before the SplitGateFilter script, to ensure accurate measurement error priors, as these can often be the dominant error channel for quantum hardware, and should be accurately modelled to infer other error rates.
We first will run, for example, 40 pi/2 X rotation gates which we have already applied to every qubit on a certain QPU device and gathered into a file named READOUT_40_X_EXAMPLE.csv.
# Loading in the order of qubit measurement:
qubit_order = np.loadtxt('./2-qubit-data/READOUT_40_X_EXAMPLE_MEASURED_QUBITS.csv', delimiter=',')
# Define gate type being measured
gate_type = 'X'
# Define number of gates applied to each qubit
gate_num = 40
# Define the file name of our data under `interested_circuits`
interested_circuit = ['READOUT_40_X_EXAMPLE_MEASURED_QUBITS.csv']
my_gate_filter.inference(qubit_orders=[qubit_order],
gate_type=gate_type,
gate_num=gate_num,
interested_circuits=interested_circuits,
# qubit_couplings=[],
# ^Left empty, only used for 2-qubit inference
nPrior = 40000,
meas_sd=0.05,
gate_sd=0.01,
seed=54,
use_informed_priors=True,
plotting=True)
Produces State0_Post_Qubit0.csv etc. Also produces with the variables:
gate_filter.post['Qubit0'] -- The full $40000 \times 3$ posterior matrix.
gate_filter.means['Qubit0'] -- The average error rates [Meas0, Meas1, Gate].
To output the fidelity of qubit, both towards and from the ground state, along with the gate error, return gate_filter.mean()['Qubit0'] or similar for any number Qubit.
Denoising
It is difficult and often intractable to denoise using the gate bitflip errors, the best method would be to compile qubits to utilise their least noisy connections, and gates, thus minimising errors.
However, a kind of efficient denoiser of measurement error in polynomial time is possible, by assuming that measurement error is not correlated (thus, ignoring cross-talk). Such a method is here developed for use of denoising quantum bitstring outputs to better estimate the final quantum state. To denoise a given dictionary of bitstring counts, infer the measurement error rates using the SplitMeasFilter class along with gathered data, and run the following:
from src.splitmeasfilter import SplitMeasFilter
qubits = [0,1,...]
meas_filter = SplitMeasFilter(qubit_order=qubits, file_address='./data/meas_cal')
data_example = {
"000": 100,
"001": 50,
"010": 75,
"011": 25,
"111": 1,
}
# Here the eff_DeNoise() will take a dictionary of bitstrings and their counts, as with `data_example`, and return a denoised data dictionary
# `verbose` gives continual confirmation, GD gives an overkill gradient descent error mitigation technique,
# with lr the learning rate, and max_iter giving the maximum GD iterations
denoised_date = meas_filter.eff_DeNoise(datadict=data_example, percentage=100, verbose=True, GD = False, lr=0.1, max_iter=50)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unnoise_compile-0.1.0.tar.gz.
File metadata
- Download URL: unnoise_compile-0.1.0.tar.gz
- Upload date:
- Size: 36.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1a4ab41253886c9c99d1a48fae686e5100d5491f5cbe691278e3c1a2fa67424
|
|
| MD5 |
c857f600a2cf24ae71b10cc6b2730818
|
|
| BLAKE2b-256 |
b6d4ed5d6086cae594dc2952b0793d1e8cd2e8a4f6bc6a930ac31c50dd9d6a6b
|
Provenance
The following attestation bundles were made for unnoise_compile-0.1.0.tar.gz:
Publisher:
python-package.yml on judemetcalf111/UnNoiseCompile
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unnoise_compile-0.1.0.tar.gz -
Subject digest:
b1a4ab41253886c9c99d1a48fae686e5100d5491f5cbe691278e3c1a2fa67424 - Sigstore transparency entry: 935888302
- Sigstore integration time:
-
Permalink:
judemetcalf111/UnNoiseCompile@757a6fbe50b7b1a67bc5d710350cafdb7d6f1cb2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/judemetcalf111
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yml@757a6fbe50b7b1a67bc5d710350cafdb7d6f1cb2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file unnoise_compile-0.1.0-py3-none-any.whl.
File metadata
- Download URL: unnoise_compile-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62996a70556d24acd507cce99d88baeb5b495147b7fd04eba7513556c9cc60d6
|
|
| MD5 |
d2e07129e25544ac577a42ebb9e82783
|
|
| BLAKE2b-256 |
c6c3ae733611372d9019b3ff44584de4063ca0fe3984e8b70ac94c1b70cea162
|
Provenance
The following attestation bundles were made for unnoise_compile-0.1.0-py3-none-any.whl:
Publisher:
python-package.yml on judemetcalf111/UnNoiseCompile
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unnoise_compile-0.1.0-py3-none-any.whl -
Subject digest:
62996a70556d24acd507cce99d88baeb5b495147b7fd04eba7513556c9cc60d6 - Sigstore transparency entry: 935888372
- Sigstore integration time:
-
Permalink:
judemetcalf111/UnNoiseCompile@757a6fbe50b7b1a67bc5d710350cafdb7d6f1cb2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/judemetcalf111
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yml@757a6fbe50b7b1a67bc5d710350cafdb7d6f1cb2 -
Trigger Event:
push
-
Statement type: