Repository: cda-tum/mqt-qecc (language: python)

Issue:
🏁 Windows Support
## Description

This PR adds continuous testing and deployment for Windows.
To this end, it switches to the reusable MQT workflows for CI and CD.
Along the way, it fixes a couple of errors that only revealed themselves on Windows.

## Checklist:

<!---
This checklist serves as a reminder of a couple of things that ensure your pull request will be merged swiftly.
-->

- [x] The pull request only contains commits that are related to it.
- [x] I have added appropriate tests and documentation.
- [x] I have made sure that all CI jobs on GitHub pass.
- [x] The pull request introduces no new warnings and follows the project's style guidelines.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 20a339a2..2db8b96b 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -31,7 +31,7 @@ jobs:
           path: dist
           merge-multiple: true
       - name: Generate artifact attestation for sdist and wheel(s)
-        uses: actions/attest-build-provenance@v1.4.0
+        uses: actions/attest-build-provenance@v1.4.2
         with:
           subject-path: "dist/*"
       - uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 02bf4765..04e7f8a2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ concurrency:
 jobs:
   change-detection:
     name: 🔍 Change
-    uses: cda-tum/mqt-workflows/.github/workflows/reusable-change-detection.yml@v1.1.5
+    uses: cda-tum/mqt-workflows/.github/workflows/reusable-change-detection.yml@v1.3
 
   cpp-tests:
     name: 🇨‌ Test
diff --git a/.github/workflows/reusable-python-ci.yml b/.github/workflows/reusable-python-ci.yml
index 78c25682..ab2538b7 100644
--- a/.github/workflows/reusable-python-ci.yml
+++ b/.github/workflows/reusable-python-ci.yml
@@ -41,14 +41,14 @@ jobs:
       fail-fast: false
       matrix:
         runs-on: [ubuntu-latest]
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
         include:
           - runs-on: macos-13
-            python-version: "3.8"
+            python-version: "3.9"
           - runs-on: macos-13
             python-version: "3.12"
           - runs-on: macos-14
-            python-version: "3.10"
+            python-version: "3.9"
           - runs-on: macos-14
             python-version: "3.12"
     steps:
diff --git a/.github/workflows/reusable-python-packaging.yml b/.github/workflows/reusable-python-packaging.yml
index f6a5f3fd..b2985efa 100644
--- a/.github/workflows/reusable-python-packaging.yml
+++ b/.github/workflows/reusable-python-packaging.yml
@@ -49,7 +49,7 @@ jobs:
       - if: runner.os == 'macOS'
         uses: yezz123/setup-uv@v4
       - name: Build wheels
-        uses: pypa/cibuildwheel@v2.19
+        uses: pypa/cibuildwheel@v2.20
       - name: Verify clean directory
         run: git diff --exit-code
         shell: bash
diff --git a/.github/workflows/update-mqt-core.yml b/.github/workflows/update-mqt-core.yml
index a76522ea..4f48ee10 100644
--- a/.github/workflows/update-mqt-core.yml
+++ b/.github/workflows/update-mqt-core.yml
@@ -21,6 +21,6 @@ concurrency:
 jobs:
   update-mqt-core:
     name: ⬆️ Update MQT Core
-    uses: cda-tum/mqt-workflows/.github/workflows/reusable-mqt-core-update.yml@v1.1.5
+    uses: cda-tum/mqt-workflows/.github/workflows/reusable-mqt-core-update.yml@v1.3
     with:
-      update-to-head: ${{ fromJSON(github.event.inputs.update-to-head) || false }}
+      update-to-head: ${{ github.event.inputs.update-to-head || false }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 53a9739b..98eec9d6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -43,7 +43,7 @@ repos:
 
   # Handling unwanted unicode characters
   - repo: https://github.com/sirosen/texthooks
-    rev: 0.6.6
+    rev: 0.6.7
     hooks:
       - id: fix-ligatures
       - id: fix-smartquotes
@@ -58,13 +58,11 @@ repos:
 
   #  Python linting and formatting using ruff
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.5.7
+    rev: v0.6.2
     hooks:
       - id: ruff
         args: ["--fix", "--show-fixes"]
-        types_or: [python, pyi, jupyter]
       - id: ruff-format
-        types_or: [python, pyi, jupyter]
 
   # Also run Black on examples in the documentation
   - repo: https://github.com/adamchainz/blacken-docs
@@ -75,7 +73,7 @@ repos:
 
   # Check static types with mypy
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.11.1
+    rev: v1.11.2
     hooks:
       - id: mypy
         files: ^(src/mqt|test/python)
@@ -85,11 +83,10 @@ repos:
           - numpy
           - pytest
           - pytest-mock
-          - numba
 
   # Check for spelling
   - repo: https://github.com/crate-ci/typos
-    rev: v1.23.6
+    rev: v1.24.1
     hooks:
       - id: typos
 
@@ -127,14 +124,14 @@ repos:
 
   # Check best practices for scientific Python code
   - repo: https://github.com/scientific-python/cookie
-    rev: 2024.04.23
+    rev: 2024.08.19
     hooks:
       - id: sp-repo-review
         additional_dependencies: ["repo-review[cli]"]
 
   # Check JSON schemata
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.29.1
+    rev: 0.29.2
     hooks:
       - id: check-dependabot
       - id: check-github-workflows
@@ -142,6 +139,6 @@ repos:
 
   # Check the pyproject.toml file
   - repo: https://github.com/henryiii/validate-pyproject-schema-store
-    rev: 2024.08.08
+    rev: 2024.08.26
     hooks:
       - id: validate-pyproject
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c0144839..c05e0585 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.19...3.28)
+cmake_minimum_required(VERSION 3.19...3.30)
 
 project(
   mqt-qecc
diff --git a/README.md b/README.md
index 33a2bb31..0fa74879 100644
--- a/README.md
+++ b/README.md
@@ -163,6 +163,7 @@ The contributors to this tool are:
 - Thomas Grurl
 - Peter-Jan H.S. Derks
 - Timo Hillmann
+- Tom Peham
 
 ## Acknowledgements
 
diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake
index cd28d503..6fcfb1c8 100644
--- a/cmake/ExternalDependencies.cmake
+++ b/cmake/ExternalDependencies.cmake
@@ -16,13 +16,13 @@ if(BUILD_MQT_QECC_BINDINGS)
   endif()
 
   # add pybind11 library
-  find_package(pybind11 CONFIG REQUIRED)
+  find_package(pybind11 2.13 CONFIG REQUIRED)
 endif()
 
 # cmake-format: off
-set(MQT_CORE_VERSION 2.5.1
+set(MQT_CORE_VERSION 2.6.1
     CACHE STRING "MQT Core version")
-set(MQT_CORE_REV "35e06ca3067ca3cf36bda1f0c38edf5bd7456fb6"
+set(MQT_CORE_REV "5be1c3ec4efb773d0330298621704e876afa7c16"
     CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
 set(MQT_CORE_REPO_OWNER "cda-tum"
     CACHE STRING "MQT Core repository owner (change when using a fork)")
diff --git a/include/DecodingSimulator.hpp b/include/DecodingSimulator.hpp
index 326a124f..8cc2d3ac 100644
--- a/include/DecodingSimulator.hpp
+++ b/include/DecodingSimulator.hpp
@@ -1,18 +1,16 @@
-//
-// Created by luca on 09/08/22.
-//
+#pragma once
 
-#ifndef QECC_DECODINGSIMULATOR_HPP
-#define QECC_DECODINGSIMULATOR_HPP
-
-#include "Decoder.hpp"
-#include "UFHeuristic.hpp"
+#include "Code.hpp"
 
+#include <cstddef>
+#include <cstdint>
+#include <nlohmann/json.hpp>
+#include <stdexcept>
 #include <string>
-#include <utility>
-using json = nlohmann::json;
 
-enum DecoderType {
+using json = nlohmann::basic_json<>;
+
+enum DecoderType : std::uint8_t {
     UfHeuristic,
     UfDecoder
 };
@@ -25,7 +23,8 @@ enum DecoderType {
     }
     throw std::invalid_argument("Invalid decodinger type: " + status);
 }
-NLOHMANN_JSON_SERIALIZE_ENUM(DecoderType, {{UfHeuristic, "UF_HEURISTIC"}, // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays,misc-include-cleaner)
+NLOHMANN_JSON_SERIALIZE_ENUM(DecoderType, {{UfHeuristic, "UF_HEURISTIC"},
                                            {UfDecoder, "UF_DECODER"}})
 
 class DecodingSimulator {
@@ -72,5 +71,3 @@ class DecodingSimulator {
                                        std::size_t        nrSamples,
                                        const DecoderType& decoderType);
 };
-
-#endif // QECC_DECODINGSIMULATOR_HPP
diff --git a/include/ecc/Ecc.hpp b/include/ecc/Ecc.hpp
index 7555bd7a..6dbcaee0 100644
--- a/include/ecc/Ecc.hpp
+++ b/include/ecc/Ecc.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
 #include "Definitions.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/OpType.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/OpType.hpp"
 
 #include <cstddef>
 #include <cstdint>
diff --git a/include/ecc/Id.hpp b/include/ecc/Id.hpp
index 9c473e6d..0ace9505 100644
--- a/include/ecc/Id.hpp
+++ b/include/ecc/Id.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <cstddef>
 #include <memory>
diff --git a/include/ecc/Q18Surface.hpp b/include/ecc/Q18Surface.hpp
index 125331e8..4d564d9a 100644
--- a/include/ecc/Q18Surface.hpp
+++ b/include/ecc/Q18Surface.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/include/ecc/Q3Shor.hpp b/include/ecc/Q3Shor.hpp
index 5eb6bc87..c8eda9a9 100644
--- a/include/ecc/Q3Shor.hpp
+++ b/include/ecc/Q3Shor.hpp
@@ -2,10 +2,10 @@
 
 #include "Definitions.hpp"
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Control.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <cstddef>
 #include <memory>
diff --git a/include/ecc/Q5Laflamme.hpp b/include/ecc/Q5Laflamme.hpp
index 092af904..87e2cbb0 100644
--- a/include/ecc/Q5Laflamme.hpp
+++ b/include/ecc/Q5Laflamme.hpp
@@ -1,9 +1,9 @@
 #pragma once
 
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/include/ecc/Q7Steane.hpp b/include/ecc/Q7Steane.hpp
index 7a645183..7377976f 100644
--- a/include/ecc/Q7Steane.hpp
+++ b/include/ecc/Q7Steane.hpp
@@ -2,10 +2,10 @@
 
 #include "Definitions.hpp"
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Control.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/include/ecc/Q9Shor.hpp b/include/ecc/Q9Shor.hpp
index 3e622e9d..5e511099 100644
--- a/include/ecc/Q9Shor.hpp
+++ b/include/ecc/Q9Shor.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <cstddef>
 #include <memory>
diff --git a/include/ecc/Q9Surface.hpp b/include/ecc/Q9Surface.hpp
index eeb41f2f..77c537ce 100644
--- a/include/ecc/Q9Surface.hpp
+++ b/include/ecc/Q9Surface.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
 #include "Ecc.hpp"
-#include "QuantumComputation.hpp"
-#include "operations/Operation.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/noxfile.py b/noxfile.py
index 7202a446..7a61564b 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -18,7 +18,7 @@
 
 nox.options.sessions = ["lint", "tests"]
 
-PYTHON_ALL_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"]
+PYTHON_ALL_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"]
 
 # The following lists all the build requirements for building the package.
 # Note that this includes transitive build dependencies of package dependencies,
@@ -26,9 +26,9 @@
 # and get better caching performance. This only concerns dependencies that are
 # not available via wheels on PyPI (i.e., only as source distributions).
 BUILD_REQUIREMENTS = [
-    "scikit-build-core[pyproject]>=0.8.1",
+    "scikit-build-core[pyproject]>=0.10.1",
     "setuptools_scm>=7",
-    "pybind11>=2.12",
+    "pybind11>=2.13",
     "wheel>=0.40",  # transitive dependency of pytest on Windows
     "Cython>=3; python_version > '3.11'",  # required to build ldpc on Python 3.12+
     "numpy>=1.26,<2; python_version > '3.11'",  # required to build ldpc on Python 3.12+
@@ -73,7 +73,7 @@ def _run_tests(
 
     session.install(*BUILD_REQUIREMENTS, *install_args, env=env)
     install_arg = f"-ve.[{','.join(_extras)}]"
-    session.install("--no-build-isolation", install_arg, *install_args, env=env)
+    session.install("--no-build-isolation", "--reinstall-package", "mqt.qecc", install_arg, *install_args, env=env)
     session.run("pytest", *run_args, *posargs, env=env)
 
 
@@ -104,7 +104,7 @@ def docs(session: nox.Session) -> None:
     serve = args.builder == "html" and session.interactive
     extra_installs = ["sphinx-autobuild"] if serve else []
     session.install(*BUILD_REQUIREMENTS, *extra_installs)
-    session.install("--no-build-isolation", "-ve.[docs]")
+    session.install("--no-build-isolation", "-ve.[docs]", "--reinstall-package", "mqt.qecc")
     session.chdir("docs")
 
     if args.builder == "linkcheck":
diff --git a/pyproject.toml b/pyproject.toml
index f150c37e..3fe59442 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["scikit-build-core>=0.8.1", "setuptools-scm>=7", "pybind11>=2.12"]
+requires = ["scikit-build-core>=0.10.1", "setuptools-scm>=7", "pybind11>=2.13"]
 build-backend = "scikit_build_core.build"
 
 [project]
@@ -10,7 +10,8 @@ authors = [
     { name = "Lucas Berent", email = "lucas.berent@tum.de" },
     { name = "Lukas Burgholzer", email = "lukas.burgholzer@tum.de" },
     { name = "Peter-Jan H.S. Derks", email = "peter-janderks@hotmail.com" },
-    { name = "Timo Hillmann", email = "timo.hillmann@rwth-aachen.de"}
+    { name = "Timo Hillmann", email = "timo.hillmann@rwth-aachen.de"},
+    { name = "Tom Peham", email = "tom.peham@tum.de" },
 ]
 keywords = ["MQT", "quantum-computing", "error-correction", "MaxSAT", "QLDPC"]
 license = { file = "LICENSE" }
@@ -27,15 +28,15 @@ classifiers = [
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
     "Development Status :: 5 - Production/Stable",
     "Typing :: Typed",
 ]
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 dependencies = [
     "z3-solver>=4.12,<4.14",
     "qecsim",
@@ -43,13 +44,14 @@ dependencies = [
     "numpy>=1.26,<2; python_version > '3.11'", # some of our dependencies are not yet compatible with numpy 2.0
     "numpy>=1.24,<2; python_version <= '3.11'", # some of our dependencies are not yet compatible with numpy 2.0
     "qiskit[qasm3-import]>=1.0.0",
-    "qiskit-aer>=0.14.0",
+    "qiskit-aer>=0.15.0",
     "stim >= 1.13.0",
     "multiprocess >= 0.70.16",
     "bposd>=1.6",
     "numba>=0.59; python_version > '3.11'",
     "numpy>=0.57; python_version <= '3.11'",
-    "pymatching>=2",
+    "pymatching>=2.2; python_version > '3.11'",
+    "pymatching>=2; python_version <= '3.11'",
 ]
 dynamic = ["version"]
 
@@ -87,23 +89,22 @@ Discussions = "https://github.com/cda-tum/mqt-qecc/discussions"
 
 [tool.scikit-build]
 # Protect the configuration against future changes in scikit-build-core
-minimum-version = "0.8.1"
+minimum-version = "build-system.requires"
 
 # Set the wheel install directory
 wheel.install-dir = "mqt/qecc"
 
-# Set required CMake and Ninja versions
-cmake.version = ">=3.19"
+# Set required Ninja version
 ninja.version = ">=1.10"
 
 # Setuptools-style build caching in a local directory
-build-dir = "build/{wheel_tag}"
+build-dir = "build/{build_type}"
 
 # Explicitly set the package directory
 wheel.packages = ["src/mqt"]
 
 # Only build the Python bindings target
-cmake.targets = ["pyqecc"]
+build.targets = ["pyqecc"]
 
 # Only install the Python package component
 install.components = ["mqt-qecc_Python"]
@@ -145,11 +146,12 @@ addopts = ["-ra", "--strict-markers", "--strict-config", "--showlocals"]
 log_cli_level = "INFO"
 xfail_strict = true
 filterwarnings = [
-#    "error",  # temporary fix until https://github.com/quantumgizmos/ldpc/pull/42 is merged and released
+    "error",
     "ignore:.*pkg_resources.*:DeprecationWarning:",
     "ignore:.*The retworkx package is deprecated*:DeprecationWarning:pymatching",
-    'ignore:.*Qiskit with Python 3.8.*:DeprecationWarning:',
     'ignore:.*qiskit.providers.provider.Provider.*:DeprecationWarning:',
+    'ignore:.*invalid escape sequence.*:DeprecationWarning:',
+    'ignore:.*invalid escape sequence.*:SyntaxWarning:',
 ]
 
 [tool.coverage]
@@ -164,7 +166,7 @@ report.exclude_also = [
 [tool.mypy]
 files = ["src/mqt", "test/python"]
 mypy_path = ["$MYPY_CONFIG_FILE_DIR/src"]
-python_version = "3.8"
+python_version = "3.9"
 warn_unused_configs = true
 enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
 strict = true
@@ -182,9 +184,12 @@ module = ["qiskit.*", "qecsim.*","qiskit_aer.*", "matplotlib.*", "scipy.*", "ldp
 ignore_missing_imports = true
 
 
+[tool.check-wheel-contents]
+ignore = ["W002"]  # Triggers on __init__.py's and duplicate data files
+
+
 [tool.ruff]
 line-length = 120
-src = ["src"]
 namespace-packages = ["mqt"]
 preview = true
 unsafe-fixes = true
@@ -280,20 +285,14 @@ aer = "aer"
 anc = "anc"
 
 
-[tool.repo-review]
-ignore = [
-  "PC160", # "Uses codespell" -> switched to https://github.com/crate-ci/typos
-  "PC180", # "Uses prettier" -> switched to different prettier-mirror that is not recognized by the check
-]
-
-
 [tool.cibuildwheel]
 build = "cp3*"
 skip = "*-musllinux*"
 archs = "auto64"
 test-command = "python -c \"from mqt import qecc\""
-test-skip = "*-macosx_arm64-*"
+test-skip = ["cp38-macosx_arm64", "cp313*"] # skip testing on Python 3.13 until our dependencies are ready
 build-frontend = "build[uv]"
+free-threaded-support = true
 
 [tool.cibuildwheel.linux]
 environment = { DEPLOY = "ON" }
@@ -302,9 +301,9 @@ environment = { DEPLOY = "ON" }
 environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
 
 [tool.cibuildwheel.windows]
-before-build = "uv pip install delvewheel>=1.4.0"
+before-build = "uv pip install delvewheel>=1.7.3"
 repair-wheel-command = "delvewheel repair -v -w {dest_dir} {wheel} --namespace-pkg mqt"
-environment = { CMAKE_ARGS = "-T ClangCL" }
+environment = { CMAKE_ARGS = "-T ClangCL", SKBUILD_CMAKE_ARGS="--fresh" }
 
 [[tool.cibuildwheel.overrides]]
 select = "cp312-*"
diff --git a/src/DecodingSimulator.cpp b/src/DecodingSimulator.cpp
index e5431e28..232def83 100644
--- a/src/DecodingSimulator.cpp
+++ b/src/DecodingSimulator.cpp
@@ -1,12 +1,25 @@
-//
-// Created by luca on 09/08/22.
-//
 #include "DecodingSimulator.hpp"
 
+#include "Code.hpp"
+#include "Decoder.hpp"
 #include "DecodingRunInformation.hpp"
 #include "UFDecoder.hpp"
+#include "UFHeuristic.hpp"
+#include "Utils.hpp"
 
+#include <cstddef>
+#include <ctime>
+#include <exception>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <memory>
 #include <sstream>
+#include <string>
+#include <vector>
 
 std::string generateOutFileName(const std::string& filepath) {
     auto               t  = std::time(nullptr);
@@ -34,12 +47,12 @@ void DecodingSimulator::simulateWER(const std::string& rawDataOutputFilepath,
     if (rawOut) {
         auto dataFileName = generateOutFileName(rawDataOutputFilepath);
         rawDataOutput.open(dataFileName);
-        std::cout << "Writing raw data to " << dataFileName << std::endl;
+        std::cout << "Writing raw data to " << dataFileName << '\n';
     }
     if (statsOut) {
         auto jsonFileName = generateOutFileName(statsOutputFilepath);
         statisticsOutstr.open(jsonFileName);
-        std::cout << "Writing stats output to " << jsonFileName << std::endl;
+        std::cout << "Writing stats output to " << jsonFileName << '\n';
         statisticsOutstr << "{ \"runs\" : [ ";
         statisticsOutstr << R"({ "run": { "physicalErrRate":)" << minPhysicalErrRate << ", \"data\": [ ";
     }
@@ -108,13 +121,13 @@ void DecodingSimulator::simulateAverageRuntime(const std::string& rawDataOutputF
     std::ofstream dataOutStream;
 
     if (rawOut) {
-        std::cout << "writing raw data to " << rawDataOutputFilepath << std::endl;
+        std::cout << "writing raw data to " << rawDataOutputFilepath << '\n';
         finalRawOut.open(generateOutFileName(rawDataOutputFilepath));
         finalRawOut.rdbuf()->pubsetbuf(nullptr, 0);
     }
 
     if (infoOut) {
-        std::cout << "writing statistics to " << decodingInfoOutfilePath << std::endl;
+        std::cout << "writing statistics to " << decodingInfoOutfilePath << '\n';
         dataOutStream.open(generateOutFileName(decodingInfoOutfilePath));
         dataOutStream.rdbuf()->pubsetbuf(nullptr, 0);
     }
@@ -125,7 +138,7 @@ void DecodingSimulator::simulateAverageRuntime(const std::string& rawDataOutputF
 
     DecodingRunInformation info;
     for (const auto& file : std::filesystem::directory_iterator(codesPath)) {
-        codePaths.emplace_back(file.path());
+        codePaths.emplace_back(file.path().string());
     }
     try {
         for (const auto& currPath : codePaths) {
@@ -165,7 +178,7 @@ void DecodingSimulator::simulateAverageRuntime(const std::string& rawDataOutputF
             avgSampleRuns = {};
         }
     } catch (std::exception& e) {
-        std::cerr << "Exception occurred " << e.what() << std::endl;
+        std::cerr << "Exception occurred " << e.what() << '\n';
     }
     if (rawOut) {
         const json j = avgSampleRunsPerCode;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index eb02bf0e..98cb466d 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -187,7 +187,7 @@ void Utils::printTimePerSampleRun(const std::map<std::string, std::size_t, std::
 
 void Utils::readInFilePathsFromDirectory(const std::string& inPath, std::vector<std::string>& codePaths) {
     for (const auto& file : std::filesystem::directory_iterator(inPath)) {
-        codePaths.emplace_back(file.path());
+        codePaths.emplace_back(file.path().string());
     }
 }
 
diff --git a/src/ecc/CMakeLists.txt b/src/ecc/CMakeLists.txt
index 6748c95e..45bcf018 100644
--- a/src/ecc/CMakeLists.txt
+++ b/src/ecc/CMakeLists.txt
@@ -13,8 +13,12 @@ if(NOT TARGET ${PROJECT_NAME}-ecc)
     Q9Surface.cpp
     Q18Surface.cpp)
 
-  target_link_libraries(${PROJECT_NAME}-ecc PUBLIC MQT::Core)
-  target_link_libraries(${PROJECT_NAME}-ecc PRIVATE MQT::ProjectOptions MQT::ProjectWarnings)
+  target_link_libraries(
+    ${PROJECT_NAME}-ecc
+    PUBLIC MQT::CoreIR
+    PRIVATE MQT::ProjectOptions MQT::ProjectWarnings)
+
+  target_include_directories(${PROJECT_NAME}-ecc PUBLIC ${MQT_QECC_INCLUDE_BUILD_DIR})
 
   # add MQT alias
   add_library(MQT::QECCFramework ALIAS ${PROJECT_NAME}-ecc)
diff --git a/src/ecc/Ecc.cpp b/src/ecc/Ecc.cpp
index d42618ca..2a1a168d 100644
--- a/src/ecc/Ecc.cpp
+++ b/src/ecc/Ecc.cpp
@@ -1,6 +1,6 @@
 #include "ecc/Ecc.hpp"
 
-#include "QuantumComputation.hpp"
+#include "ir/QuantumComputation.hpp"
 
 #include <cstddef>
 #include <memory>
diff --git a/src/ecc/Q18Surface.cpp b/src/ecc/Q18Surface.cpp
index 36cf911d..ff44ba5d 100644
--- a/src/ecc/Q18Surface.cpp
+++ b/src/ecc/Q18Surface.cpp
@@ -1,10 +1,10 @@
 #include "ecc/Q18Surface.hpp"
 
 #include "ecc/Ecc.hpp"
-#include "operations/Control.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/ecc/Q3Shor.cpp b/src/ecc/Q3Shor.cpp
index f03f3b83..42490cca 100644
--- a/src/ecc/Q3Shor.cpp
+++ b/src/ecc/Q3Shor.cpp
@@ -2,10 +2,10 @@
 
 #include "Definitions.hpp"
 #include "ecc/Ecc.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
-#include "operations/StandardOperation.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
+#include "ir/operations/StandardOperation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/ecc/Q5Laflamme.cpp b/src/ecc/Q5Laflamme.cpp
index 999933f2..1e40ef5b 100644
--- a/src/ecc/Q5Laflamme.cpp
+++ b/src/ecc/Q5Laflamme.cpp
@@ -1,10 +1,10 @@
 #include "ecc/Q5Laflamme.hpp"
 
 #include "ecc/Ecc.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
-#include "operations/StandardOperation.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
+#include "ir/operations/StandardOperation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/ecc/Q7Steane.cpp b/src/ecc/Q7Steane.cpp
index 36ae0eb7..6eb077b9 100644
--- a/src/ecc/Q7Steane.cpp
+++ b/src/ecc/Q7Steane.cpp
@@ -2,11 +2,11 @@
 
 #include "Definitions.hpp"
 #include "ecc/Ecc.hpp"
-#include "operations/Control.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
-#include "operations/StandardOperation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
+#include "ir/operations/StandardOperation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/ecc/Q9Shor.cpp b/src/ecc/Q9Shor.cpp
index eda8518f..4ba521c8 100644
--- a/src/ecc/Q9Shor.cpp
+++ b/src/ecc/Q9Shor.cpp
@@ -1,11 +1,11 @@
 #include "ecc/Q9Shor.hpp"
 
 #include "ecc/Ecc.hpp"
-#include "operations/Control.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
-#include "operations/StandardOperation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
+#include "ir/operations/StandardOperation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/ecc/Q9Surface.cpp b/src/ecc/Q9Surface.cpp
index 86683751..43224507 100644
--- a/src/ecc/Q9Surface.cpp
+++ b/src/ecc/Q9Surface.cpp
@@ -1,10 +1,10 @@
 #include "ecc/Q9Surface.hpp"
 
 #include "ecc/Ecc.hpp"
-#include "operations/Control.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
-#include "operations/Operation.hpp"
+#include "ir/operations/Control.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
+#include "ir/operations/Operation.hpp"
 
 #include <array>
 #include <cstddef>
diff --git a/src/mqt/qecc/analog_information_decoding/simulators/analog_tannergraph_decoding.py b/src/mqt/qecc/analog_information_decoding/simulators/analog_tannergraph_decoding.py
index 90b54dd9..a85807fe 100644
--- a/src/mqt/qecc/analog_information_decoding/simulators/analog_tannergraph_decoding.py
+++ b/src/mqt/qecc/analog_information_decoding/simulators/analog_tannergraph_decoding.py
@@ -90,7 +90,7 @@ def _set_analog_syndrome(self, analog_syndrome: NDArray[np.float64]) -> None:
     def decode(self, analog_syndrome: NDArray[np.float64]) -> NDArray[np.int32]:
         """Decode a given analog syndrome."""
         self._set_analog_syndrome(analog_syndrome)
-        return self.bposd_decoder.decode(simulation_utils.get_binary_from_analog(analog_syndrome))  # type: ignore[no-any-return]
+        return self.bposd_decoder.decode(simulation_utils.get_binary_from_analog(analog_syndrome))
 
 
 class AtdSimulator:
diff --git a/src/mqt/qecc/analog_information_decoding/simulators/memory_experiment_v2.py b/src/mqt/qecc/analog_information_decoding/simulators/memory_experiment_v2.py
index 1ded3773..233fda65 100644
--- a/src/mqt/qecc/analog_information_decoding/simulators/memory_experiment_v2.py
+++ b/src/mqt/qecc/analog_information_decoding/simulators/memory_experiment_v2.py
@@ -158,4 +158,4 @@ def decode_multiround(
         # correct in the commit and tentative region as the last round stabilizer is perfect
         decoded = (np.cumsum(space_correction, 1) % 2)[:, -1]
 
-    return decoded.astype(np.int32), syndrome, analog_syndr, bp_iter  # type: ignore[return-value]
+    return decoded.astype(np.int32), syndrome, analog_syndr, bp_iter
diff --git a/src/mqt/qecc/analog_information_decoding/simulators/simulation.py b/src/mqt/qecc/analog_information_decoding/simulators/simulation.py
index 1fbc9402..c46a8d47 100644
--- a/src/mqt/qecc/analog_information_decoding/simulators/simulation.py
+++ b/src/mqt/qecc/analog_information_decoding/simulators/simulation.py
@@ -238,9 +238,9 @@ def _get_noisy_syndrome(
                 z_syndrome_w_err = get_noisy_analog_syndrome(perfect_syndr=z_syndrome, sigma=self.sigma_z)
             else:  # usual pauli error channel syndrome error
                 x_syndrome_err = generate_syndr_err(channel_probs=self.x_syndr_error_channel)
-                x_syndrome_w_err = (x_syndrome + x_syndrome_err) % 2  # type: ignore[assignment] # only occurs due to reused var name
+                x_syndrome_w_err = (x_syndrome + x_syndrome_err) % 2
                 z_syndrome_err = generate_syndr_err(channel_probs=self.z_syndr_error_channel)
-                z_syndrome_w_err = (z_syndrome + z_syndrome_err) % 2  # type: ignore[assignment] # only occurs due to reused var name
+                z_syndrome_w_err = (z_syndrome + z_syndrome_err) % 2
         else:
             x_syndrome_w_err = np.copy(x_syndrome)
             z_syndrome_w_err = np.copy(z_syndrome)
@@ -354,7 +354,7 @@ def _decode_ss_with_meta(
                 meta_bin = (meta_pcm @ get_binary_from_analog(syndrome_w_err)) % 2
                 meta_syndr = get_signed_from_binary(meta_bin)  # for AI decoder we need {-1,+1} syndrome as input
             else:
-                meta_syndr = (meta_pcm @ syndrome_w_err) % 2  # type: ignore[assignment] # only occurs due to reused var name
+                meta_syndr = (meta_pcm @ syndrome_w_err) % 2
 
             ss_syndr = np.hstack((syndrome_w_err, meta_syndr))
             # only first n bit are data, the other are virtual nodes and can be discarded for estimate
@@ -383,7 +383,7 @@ def _ss_analog_tg_decoding(
         meta_syndr = (meta_pcm @ bin_syndr) % 2
         ss_syndr = np.hstack((bin_syndr, meta_syndr))
         # only first n bit are data, return them
-        return decoder.decode(ss_syndr)[: self.n]  # type: ignore[no-any-return]
+        return decoder.decode(ss_syndr)[: self.n]
 
     def _two_stage_decoding(
         self, x_syndrome_w_err: NDArray[np.float64], z_syndrome_w_err: NDArray[np.float64]
@@ -483,7 +483,7 @@ def _analog_tg_decoding(
         analog_channel = get_virtual_check_init_vals(analog_syndrome, sigma)
         decoder.update_channel_probs(np.hstack((bit_err_channel, analog_channel)))
         # only first n bit are data so only return those
-        return decoder.decode(hard_syndrome)[: self.n]  # type: ignore[no-any-return]
+        return decoder.decode(hard_syndrome)[: self.n]
 
     def _single_stage_setup(
         self,
@@ -505,11 +505,11 @@ def _single_stage_setup(
         if self.z_meta and self.Mz is not None:
             self.ss_z_pcm = build_single_stage_pcm(self.Hz, self.Mz)
         else:
-            self.ss_z_pcm = None  # type: ignore[assignment]
+            self.ss_z_pcm = None
         if self.x_meta and self.Mx is not None:
             self.ss_x_pcm = build_single_stage_pcm(self.Hx, self.Mx)
         else:
-            self.ss_x_pcm = None  # type: ignore[assignment]
+            self.ss_x_pcm = None
 
         # X-checks := (Hx|Mx) => Influenced by Z-syndrome error rate
         # ss_x_bpd used to decode X bit errors using Z-side check matrices
diff --git a/src/mqt/qecc/cc_decoder/decoder.py b/src/mqt/qecc/cc_decoder/decoder.py
index d7f36e94..a43427a1 100644
--- a/src/mqt/qecc/cc_decoder/decoder.py
+++ b/src/mqt/qecc/cc_decoder/decoder.py
@@ -164,7 +164,7 @@ def simulate_error_rate(code: ColorCode, error_rate: float, nr_sims: int, solver
             for logical in range(len(code.L)):
                 if (code.L[logical] @ residual % 2).any():
                     logical_errors[logical] += 1
-                    wt = np.sum(residual)  # compute the min weight of a logical error
+                    wt: int = np.sum(residual)  # compute the min weight of a logical error
                     if min_wt_logicals[logical] == -1 or wt < min_wt_logicals[logical]:
                         min_wt_logicals[logical] = int(wt)
                     break
diff --git a/src/mqt/qecc/codes/css_code.py b/src/mqt/qecc/codes/css_code.py
index 91f186a0..ae144d99 100644
--- a/src/mqt/qecc/codes/css_code.py
+++ b/src/mqt/qecc/codes/css_code.py
@@ -96,7 +96,7 @@ def _compute_logical(m1: npt.NDArray[np.int8] | None, m2: npt.NDArray[np.int8] |
             return logs
 
         if m2 is None:
-            return mod2.nullspace(m1).astype(np.int8)  # type: ignore[no-any-return]
+            return mod2.nullspace(m1).astype(np.int8)
 
         ker_m1 = mod2.nullspace(m1)  # compute the kernel basis of m1
         im_m2_transp = mod2.row_basis(m2)  # compute the image basis of m2
@@ -220,7 +220,7 @@ def from_code_name(code_name: str, distance: int | None = None) -> CSSCode:
         if code_name == "surface":
             if distance is None:
                 distance = 3
-            code_name += "_%d" % distance
+            code_name += f"_{distance}"
 
         if code_name in paths:
             hx = np.load(paths[code_name] / "hx.npy")
diff --git a/src/mqt/qecc/ecc_qiskit_wrapper.py b/src/mqt/qecc/ecc_qiskit_wrapper.py
index 4f3f89a2..058cdea5 100644
--- a/src/mqt/qecc/ecc_qiskit_wrapper.py
+++ b/src/mqt/qecc/ecc_qiskit_wrapper.py
@@ -167,7 +167,7 @@ def main() -> None:
 
     circ = load(open_qasm_file)
 
-    if not any(gate[0].name == "measure" for gate in circ.data):
+    if not any(gate.operation.name == "measure" for gate in circ.data):
         print("Warning: No measurement gates found in the circuit. Adding measurement gates to all qubits.")  # noqa: T201
         circ.measure_all()
 
diff --git a/src/mqt/qecc/ft_stateprep/simulation.py b/src/mqt/qecc/ft_stateprep/simulation.py
index 41342368..6759632d 100644
--- a/src/mqt/qecc/ft_stateprep/simulation.py
+++ b/src/mqt/qecc/ft_stateprep/simulation.py
@@ -51,11 +51,11 @@ def __init__(
         self.zero_state = zero_state
         # Store which measurements are X, Z or data measurements.
         # The indices refer to the indices of the measurements in the stim circuit.
-        self.x_verification_measurements = []  # type: list[int]
-        self.z_verification_measurements = []  # type: list[int]
-        self.x_measurements = []  # type: list[int]
-        self.z_measurements = []  # type: list[int]
-        self.data_measurements = []  # type: list[int]
+        self.x_verification_measurements: list[int] = []
+        self.z_verification_measurements: list[int] = []
+        self.x_measurements: list[int] = []
+        self.z_measurements: list[int] = []
+        self.data_measurements: list[int] = []
         self.parallel_gates = parallel_gates
         self.n_measurements = 0
         self.stim_circ = stim.Circuit()
@@ -98,10 +98,6 @@ def to_stim_circ(self) -> stim.Circuit:
         - Two-qubit gates are followed by a two-qubit Pauli error with probability p/15.
         - Measurements flip with a probability of 2/3 p.
         - Qubit are initialized in the -1 Eigenstate with probability 2/3 p.
-
-        Args:
-            circ: The QuantumCircuit to convert.
-            p: The error rate.
         """
         initialized = [False for _ in self.circ.qubits]
         stim_circuit = stim.Circuit()
@@ -115,10 +111,9 @@ def idle_error(used_qubits: list[int]) -> None:
 
         dag = circuit_to_dag(self.circ)
         layers = dag.layers()
-        used_qubits = []  # type: list[int]
-
+        used_qubits: list[int] = []
         targets = set()
-        measured = defaultdict(int)  # type: defaultdict[int, int]
+        measured: defaultdict[int, int] = defaultdict(int)
         for layer in layers:
             layer_circ = dag_to_circuit(layer["graph"])
 
@@ -128,8 +123,8 @@ def idle_error(used_qubits: list[int]) -> None:
 
             used_qubits = []
             for gate in layer_circ.data:
-                if gate[0].name == "h":
-                    qubit = self.circ.find_bit(gate[1][0])[0]
+                if gate.operation.name == "h":
+                    qubit = self.circ.find_bit(gate.qubits[0])[0]
                     ctrls.append(qubit)
                     if initialized[qubit]:
                         stim_circuit.append_operation("H", [qubit])
@@ -139,9 +134,9 @@ def idle_error(used_qubits: list[int]) -> None:
                         else:
                             used_qubits.append(qubit)
 
-                elif gate[0].name == "cx":
-                    ctrl = self.circ.find_bit(gate[1][0])[0]
-                    target = self.circ.find_bit(gate[1][1])[0]
+                elif gate.operation.name == "cx":
+                    ctrl = self.circ.find_bit(gate.qubits[0])[0]
+                    target = self.circ.find_bit(gate.qubits[1])[0]
                     targets.add(target)
                     if not initialized[ctrl]:
                         if ctrl in ctrls:
@@ -161,8 +156,8 @@ def idle_error(used_qubits: list[int]) -> None:
                     else:
                         used_qubits.extend([ctrl, target])
 
-                elif gate[0].name == "measure":
-                    anc = self.circ.find_bit(gate[1][0])[0]
+                elif gate.operation.name == "measure":
+                    anc = self.circ.find_bit(gate.qubits[0])[0]
                     stim_circuit.append_operation("X_ERROR", [anc], [2 * self.p / 3])
                     stim_circuit.append_operation("MR", [anc])
                     if anc in targets:
@@ -181,7 +176,7 @@ def idle_error(used_qubits: list[int]) -> None:
 
         return stim_circuit
 
-    def measure_stabilizers(self) -> stim.Circuit:
+    def measure_stabilizers(self) -> None:
         """Measure the stabilizers of the code.
 
         An ancilla is used for each measurement.
@@ -295,7 +290,7 @@ def _simulate_batch(self, shots: int = 1024) -> tuple[int, int]:
         corrected = state + estimates
 
         num_discarded = detection_events.shape[0] - filtered_events.shape[0]
-        num_logical_errors = np.sum(
+        num_logical_errors: int = np.sum(
             np.any(corrected @ observables.T % 2 != 0, axis=1)
         )  # number of non-commuting corrected states
         return num_logical_errors, num_discarded
@@ -348,8 +343,8 @@ def __init__(self, code: CSSCode, init_luts: bool = True) -> None:
             init_luts: Whether to initialize the lookup tables at object creation.
         """
         self.code = code
-        self.x_lut = {}  # type: dict[bytes, npt.NDArray[np.int8]]
-        self.z_lut = {}  # type: dict[bytes, npt.NDArray[np.int8]]
+        self.x_lut: dict[bytes, npt.NDArray[np.int8]] = {}
+        self.z_lut: dict[bytes, npt.NDArray[np.int8]] = {}
         if init_luts:
             self.generate_x_lut()
             self.generate_z_lut()
@@ -403,9 +398,9 @@ def _generate_lut(checks: npt.NDArray[np.int_]) -> dict[bytes, npt.NDArray[np.in
         n_qubits = checks.shape[1]
 
         syndromes = defaultdict(list)
-        lut = {}  # type: dict[bytes, npt.NDArray[np.int8]]
+        lut: dict[bytes, npt.NDArray[np.int8]] = {}
         for i in range(2**n_qubits):
-            state = np.array(list(np.binary_repr(i).zfill(n_qubits))).astype(np.int8)  # type: npt.NDArray[np.int_]
+            state: npt.NDArray[np.int_] = np.array(list(np.binary_repr(i).zfill(n_qubits))).astype(np.int8)
             syndrome = checks @ state % 2
             syndromes[syndrome.astype(np.int8).tobytes()].append(state)
 
diff --git a/src/mqt/qecc/ft_stateprep/state_prep.py b/src/mqt/qecc/ft_stateprep/state_prep.py
index e7abf1d3..9d8e5ba4 100644
--- a/src/mqt/qecc/ft_stateprep/state_prep.py
+++ b/src/mqt/qecc/ft_stateprep/state_prep.py
@@ -18,7 +18,6 @@
 
 logger = logging.getLogger(__name__)
 
-
 if TYPE_CHECKING:  # pragma: no cover
     from collections.abc import Callable
 
@@ -78,7 +77,6 @@ def compute_fault_set(
         """Compute the fault set of the state.
 
         Args:
-            state: The stabilizer state to compute the fault set for.
             num_errors: The number of independent errors to propagate through the circuit.
             x_errors: If True, compute the fault set for X errors. If False, compute the fault set for Z errors.
             reduce: If True, reduce the fault set by the stabilizers of the code to reduce weights.
@@ -86,7 +84,9 @@ def compute_fault_set(
         Returns:
             The fault set of the state.
         """
-        faults = self.x_fault_sets[num_errors] if x_errors else self.z_fault_sets[num_errors]  # type: npt.NDArray[np.int8] | None
+        faults: npt.NDArray[np.int8] | None = (
+            self.x_fault_sets[num_errors] if x_errors else self.z_fault_sets[num_errors]
+        )
         if faults is not None:
             return faults
 
@@ -104,7 +104,7 @@ def compute_fault_set(
             faults = np.unique(faults, axis=0)
 
             if x_errors and self.x_fault_sets_unreduced[1] is None:
-                non_propagated_single_errors = np.eye(self.num_qubits, dtype=np.int8)  # type: npt.NDArray[np.int8]
+                non_propagated_single_errors = np.eye(self.num_qubits, dtype=np.int8)
                 self.x_fault_sets_unreduced[1] = np.vstack((faults, non_propagated_single_errors))
             elif not x_errors and self.z_fault_sets[1] is None:
                 non_propagated_single_errors = np.eye(self.num_qubits, dtype=np.int8)
@@ -168,7 +168,7 @@ def combine_faults(
             next_faults = fault_sets_unreduced[i + 1]
             assert next_faults is not None
             fault_sets_unreduced[i + 1] = np.vstack((next_faults, combined))
-        fault_sets = [None for _ in range(self.max_errors + 1)]  # type: list[None | npt.NDArray[np.int8]]
+        fault_sets: list[npt.NDArray[np.int8] | None] = [None for _ in range(self.max_errors + 1)]
         stabs = self.x_checks if x_errors else self.z_checks
         for num_errors in range(1, self.max_errors + 1):
             fs = fault_sets_unreduced[num_errors]
@@ -188,10 +188,10 @@ def _set_max_errors(self) -> None:
         self.max_z_errors = (
             (self.code.z_distance - 1) // 2 if not self.error_detection_code else self.code.z_distance // 2
         )
-        self.x_fault_sets = [None for _ in range(self.max_errors + 1)]  # type: list[npt.NDArray[np.int8] | None]
-        self.z_fault_sets = [None for _ in range(self.max_errors + 1)]  # type: list[npt.NDArray[np.int8] | None]
-        self.x_fault_sets_unreduced = [None for _ in range(self.max_errors + 1)]  # type: list[npt.NDArray[np.int8] | None]
-        self.z_fault_sets_unreduced = [None for _ in range(self.max_errors + 1)]  # type: list[npt.NDArray[np.int8] | None]
+        self.x_fault_sets: list[npt.NDArray[np.int8] | None] = [None for _ in range(self.max_errors + 1)]
+        self.z_fault_sets: list[npt.NDArray[np.int8] | None] = [None for _ in range(self.max_errors + 1)]
+        self.x_fault_sets_unreduced: list[npt.NDArray[np.int8] | None] = [None for _ in range(self.max_errors + 1)]
+        self.z_fault_sets_unreduced: list[npt.NDArray[np.int8] | None] = [None for _ in range(self.max_errors + 1)]
 
 
 def heuristic_prep_circuit(code: CSSCode, optimize_depth: bool = True, zero_state: bool = True) -> StatePrepCircuit:
@@ -218,16 +218,16 @@ def is_reduced() -> bool:
     costs -= np.sum(checks, axis=0)
     np.fill_diagonal(costs, 1)
 
-    used_qubits = []  # type: list[np.int_]
-    cnots = []  # type: list[tuple[int, int]]
+    used_qubits: list[np.int_] = []
+    cnots: list[tuple[int, int]] = []
     while not is_reduced():
-        m = np.zeros((checks.shape[1], checks.shape[1]), dtype=bool)  # type: npt.NDArray[np.bool_]
+        m = np.zeros((checks.shape[1], checks.shape[1]), dtype=bool)
         m[used_qubits, :] = True
         m[:, used_qubits] = True
 
         costs_unused = np.ma.array(costs, mask=m)  # type: ignore[no-untyped-call]
         if np.all(costs_unused >= 0) or len(used_qubits) == checks.shape[1]:  # no more reductions possible
-            if used_qubits == []:  # local minimum => get out by making matrix triangular
+            if not used_qubits:  # local minimum => get out by making matrix triangular
                 logging.warning("Local minimum reached. Making matrix triangular.")
                 checks = mod2.reduced_row_echelon(checks)[0]
                 costs = np.array([
@@ -329,7 +329,7 @@ def _generate_circ_with_bounded_depth(
 
 def _generate_circ_with_bounded_gates(
     checks: npt.NDArray[np.int8], max_cnots: int, zero_state: bool = True
-) -> QuantumCircuit:
+) -> QuantumCircuit | None:
     """Find the gate optimal circuit for a given check matrix and maximum depth."""
     n = checks.shape[1]
     columns = np.array([
@@ -591,7 +591,7 @@ def gate_optimal_verification_stabilizers(
         A list of stabilizers to verify the state preparation circuit.
     """
     max_errors = sp_circ.max_errors
-    layers = [[] for _ in range(max_errors)]  # type: list[list[npt.NDArray[np.int8]]]
+    layers: list[list[npt.NDArray[np.int8]]] = [[] for _ in range(max_errors)]
     if max_ancillas is None:
         max_ancillas = sp_circ.max_z_measurements if x_errors else sp_circ.max_x_measurements
 
@@ -618,8 +618,8 @@ def gate_optimal_verification_stabilizers(
         # Minimal CNOT solution must be achievable with these
         num_anc = max_ancillas
         checks = sp_circ.z_checks if x_errors else sp_circ.x_checks
-        min_cnots = np.min(np.sum(checks, axis=1))
-        max_cnots = np.sum(checks)
+        min_cnots: int = np.min(np.sum(checks, axis=1))
+        max_cnots: int = np.sum(checks)
 
         logging.info(
             f"Finding verification stabilizers for {num_errors} errors with {min_cnots} to {max_cnots} CNOTs using {num_anc} ancillas"
@@ -700,7 +700,7 @@ def _verification_circuit(
     full_fault_tolerance: bool = True,
 ) -> QuantumCircuit:
     logging.info("Finding verification stabilizers for the state preparation circuit")
-    layers_1 = verification_stabs_fun(sp_circ, sp_circ.zero_state)  # type: ignore[call-arg]
+    layers_1 = verification_stabs_fun(sp_circ, sp_circ.zero_state, None)
     measurements_1 = [measurement for layer in layers_1 for measurement in layer]
 
     if full_fault_tolerance:
@@ -793,7 +793,7 @@ def heuristic_verification_stabilizers(
     """
     logging.info("Finding verification stabilizers using heuristic method")
     max_errors = sp_circ.max_errors
-    layers = [[] for _ in range(max_errors)]  # type: list[list[npt.NDArray[np.int8]]]
+    layers: list[list[npt.NDArray[np.int8]]] = [[] for _ in range(max_errors)]
     sp_circ.compute_fault_sets()
     fault_sets = (
         sp_circ.combine_faults(additional_faults, x_errors)
@@ -825,13 +825,10 @@ def _set_cover(
     n: int, cands: set[frozenset[int]], mapping: dict[frozenset[int], list[npt.NDArray[np.int8]]]
 ) -> set[frozenset[int]]:
     universe = set(range(n))
-    cover = set()  # type: set[frozenset[int]]
-
-    def sort_key(stab: frozenset[int], universe: set[int] = universe) -> tuple[int, np.int_]:
-        return (len(stab & universe), -np.sum(mapping[stab]))
+    cover: set[frozenset[int]] = set()
 
     while universe:
-        best = max(cands, key=sort_key)
+        best = max(cands, key=lambda stab: (len(stab & universe), -np.sum(mapping[stab])))  # type: ignore[operator]
         cover.add(best)
         universe -= best
     return cover
@@ -840,8 +837,8 @@ def sort_key(stab: frozenset[int], universe: set[int] = universe) -> tuple[int,
 def _extend_covering_sets(
     candidate_sets: set[frozenset[int]], size_limit: int, mapping: dict[frozenset[int], list[npt.NDArray[np.int8]]]
 ) -> set[frozenset[int]]:
-    to_remove = set()  # type: set[frozenset[int]]
-    to_add = set()  # type: set[frozenset[int]]
+    to_remove: set[frozenset[int]] = set()
+    to_add: set[frozenset[int]] = set()
     for c1 in candidate_sets:
         for c2 in candidate_sets:
             if len(to_add) >= size_limit:
@@ -986,7 +983,7 @@ def _measure_ft_stabs(
 
 def _vars_to_stab(
     measurement: list[z3.BoolRef | bool], generators: npt.NDArray[np.int8]
-) -> npt.NDArray[z3.BoolRef | bool]:  # type: ignore[type-var]
+) -> npt.NDArray[z3.BoolRef | bool]:
     measurement_stab = _symbolic_scalar_mult(generators[0], measurement[0])
     for i, scalar in enumerate(measurement[1:]):
         measurement_stab = _symbolic_vector_add(measurement_stab, _symbolic_scalar_mult(generators[i + 1], scalar))
@@ -1007,7 +1004,6 @@ def verification_stabilizers(
         fault_set: The set of errors to verify.
         num_anc: The maximum number of ancilla qubits to use.
         num_cnots: The maximumg number of CNOT gates to use.
-        num_errors: The number of errors occur in the state prep circuit.
         x_errors: If True, the errors are X errors. Otherwise, the errors are Z errors.
     """
     # Measurements are written as sums of generators
@@ -1040,7 +1036,7 @@ def verification_stabilizers(
         # Extract stabilizer measurements from model
         actual_measurements = []
         for m in measurement_vars:
-            v = np.zeros(sp_circ.num_qubits, dtype=np.int8)  # type: npt.NDArray[np.int8]
+            v = np.zeros(sp_circ.num_qubits, dtype=np.int8)
             for g in range(n_gens):
                 if model[m[g]]:
                     v += gens[g]
@@ -1136,7 +1132,7 @@ def _column_addition_constraint(
     col_add_vars: npt.NDArray[z3.BoolRef],
 ) -> z3.BoolRef:
     assert len(columns.shape) == 3
-    max_depth = col_add_vars.shape[0]  # type: ignore[unreachable]
+    max_depth = col_add_vars.shape[0]
     n_cols = col_add_vars.shape[2]
 
     constraints = []
@@ -1170,7 +1166,7 @@ def _column_addition_constraint(
 
 def _final_matrix_constraint(columns: npt.NDArray[z3.BoolRef | bool]) -> z3.BoolRef:
     assert len(columns.shape) == 3
-    return z3.PbEq(  # type: ignore[unreachable]
+    return z3.PbEq(
         [(z3.Not(z3.Or(list(columns[-1, :, col]))), 1) for col in range(columns.shape[2])],
         columns.shape[2] - columns.shape[1],
     )
@@ -1179,11 +1175,11 @@ def _final_matrix_constraint(columns: npt.NDArray[z3.BoolRef | bool]) -> z3.Bool
 def _propagate_error(dag: DagCircuit, node: DAGNode, x_errors: bool = True) -> PauliList:
     """Propagates a Pauli error through a circuit beginning from control of node."""
     control = node.qargs[0]._index  # noqa: SLF001
-    error = np.array([0] * dag.num_qubits(), dtype=np.int8)  # type: npt.NDArray[np.int8]
+    error: npt.NDArray[np.int8] = np.array([0] * dag.num_qubits(), dtype=np.int8)
     error[control] = 1
     # propagate error through circuit via bfs
     q = deque([node])
-    visited = set()  # type: set[DAGNode]
+    visited: set[DAGNode] = set()
     while q:
         node = q.popleft()
         if node in visited or isinstance(node, DAGOutNode):
diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp
index d05faa81..100352d5 100644
--- a/src/python/bindings.cpp
+++ b/src/python/bindings.cpp
@@ -6,7 +6,6 @@
 #include "Decoder.hpp"
 #include "DecodingRunInformation.hpp"
 #include "DecodingSimulator.hpp"
-#include "QuantumComputation.hpp"
 #include "UFDecoder.hpp"
 #include "UFHeuristic.hpp"
 #include "ecc/Ecc.hpp"
@@ -17,6 +16,7 @@
 #include "ecc/Q7Steane.hpp"
 #include "ecc/Q9Shor.hpp"
 #include "ecc/Q9Surface.hpp"
+#include "ir/QuantumComputation.hpp"
 #include "python/qiskit/QuantumCircuit.hpp"
 
 #include <pybind11/pybind11.h>
@@ -86,7 +86,7 @@ py::dict applyEcc(const py::object& circ, const std::string& eccName, const size
     return py::dict("circ"_a = oss.str());
 }
 
-PYBIND11_MODULE(pyqecc, m) {
+PYBIND11_MODULE(pyqecc, m, py::mod_gil_not_used()) {
     // In this function the exposed python functions are defined.
     // Additionally, the required parameters are described.
     m.doc() = "pybind11 for the MQT QECC quantum error-correcting codes tool";


Test patch:
diff --git a/test/python/ft_stateprep/test_simulation.py b/test/python/ft_stateprep/test_simulation.py
index d053589a..03fe2c3e 100644
--- a/test/python/ft_stateprep/test_simulation.py
+++ b/test/python/ft_stateprep/test_simulation.py
@@ -67,7 +67,7 @@ def test_lut(steane_code: CSSCode) -> None:
     assert len(lut.x_lut) != 0
     assert lut.x_lut is lut.z_lut  # Code is self dual so luts should be the same
 
-    error_1 = np.zeros(steane_code.n, dtype=np.int8)  # type: ignore[var-annotated]
+    error_1 = np.zeros(steane_code.n, dtype=np.int8)
     error_1[0] = 1
 
     error_w1 = (steane_code.Hx[0] + error_1) % 2
@@ -76,7 +76,7 @@ def test_lut(steane_code: CSSCode) -> None:
     assert steane_code.stabilizer_eq_x_error(estimate_1, error_1)
     assert steane_code.stabilizer_eq_z_error(estimate_1, error_1)
 
-    error_2 = np.zeros(steane_code.n, dtype=np.int8)  # type: ignore[var-annotated]
+    error_2 = np.zeros(steane_code.n, dtype=np.int8)
     error_2[0] = 1
     error_2[1] = 1
     error_w2 = (steane_code.Hx[0] + error_2) % 2
@@ -87,7 +87,7 @@ def test_lut(steane_code: CSSCode) -> None:
     assert not steane_code.stabilizer_eq_x_error(estimate_2, error_2)
     assert np.sum(estimate_2) == 1
 
-    error_3 = np.ones((steane_code.n), dtype=np.int8)  # type: ignore[var-annotated]
+    error_3 = np.ones((steane_code.n), dtype=np.int8)
     error_w3 = (steane_code.Hx[0] + error_3) % 2
     syndrome_3 = steane_code.get_x_syndrome(error_w3)
     estimate_3 = lut.decode_x(syndrome_3.astype(np.int8))
diff --git a/test/python/ft_stateprep/test_stateprep.py b/test/python/ft_stateprep/test_stateprep.py
index 5bb2ab17..2fbb991c 100644
--- a/test/python/ft_stateprep/test_stateprep.py
+++ b/test/python/ft_stateprep/test_stateprep.py
@@ -90,7 +90,7 @@ def color_code_d5_sp(cc_4_8_8_code: CSSCode) -> StatePrepCircuit:
 
 def eq_span(a: npt.NDArray[np.int_], b: npt.NDArray[np.int_]) -> bool:
     """Check if two matrices have the same row space."""
-    return a.shape == b.shape and mod2.rank(np.vstack((a, b))) == mod2.rank(a) == mod2.rank(b)
+    return (a.shape == b.shape) and (int(mod2.rank(np.vstack((a, b)))) == int(mod2.rank(a)) == int(mod2.rank(b)))
 
 
 def in_span(m: npt.NDArray[np.int_], v: npt.NDArray[np.int_]) -> bool:
@@ -117,14 +117,14 @@ def test_heuristic_prep_consistent(code: CSSCode, request) -> None:  # type: ign
 
     sp_circ = heuristic_prep_circuit(code)
     circ = sp_circ.circ
-    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[arg-type]
+    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[operator]
 
     assert circ.num_qubits == code.n
     assert circ.num_nonlocal_gates() <= max_cnots
 
     x, z = get_stabs(circ)
-    assert eq_span(code.Hx, x)  # type: ignore[arg-type]
-    assert eq_span(np.vstack((code.Hz, code.Lz)), z)  # type: ignore[arg-type]
+    assert eq_span(code.Hx, x)
+    assert eq_span(np.vstack((code.Hz, code.Lz)), z)
 
 
 @pytest.mark.parametrize("code", ["steane_code", "css_4_2_2_code", "css_6_2_2_code"])
@@ -136,14 +136,14 @@ def test_gate_optimal_prep_consistent(code: CSSCode, request) -> None:  # type:
     assert sp_circ.zero_state
 
     circ = sp_circ.circ
-    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[arg-type]
+    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[operator]
 
     assert circ.num_qubits == code.n
     assert circ.num_nonlocal_gates() <= max_cnots
 
     x, z = get_stabs(circ)
-    assert eq_span(code.Hx, x)  # type: ignore[arg-type]
-    assert eq_span(np.vstack((code.Hz, code.Lz)), z)  # type: ignore[arg-type]
+    assert eq_span(code.Hx, x)
+    assert eq_span(np.vstack((code.Hz, code.Lz)), z)
 
 
 @pytest.mark.parametrize("code", ["steane_code", "css_4_2_2_code", "css_6_2_2_code"])
@@ -154,14 +154,14 @@ def test_depth_optimal_prep_consistent(code: CSSCode, request) -> None:  # type:
     sp_circ = depth_optimal_prep_circuit(code, max_timeout=3)
     assert sp_circ is not None
     circ = sp_circ.circ
-    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[arg-type]
+    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[operator]
 
     assert circ.num_qubits == code.n
     assert circ.num_nonlocal_gates() <= max_cnots
 
     x, z = get_stabs(circ)
-    assert eq_span(code.Hx, x)  # type: ignore[arg-type]
-    assert eq_span(np.vstack((code.Hz, code.Lz)), z)  # type: ignore[arg-type]
+    assert eq_span(code.Hx, x)
+    assert eq_span(np.vstack((code.Hz, code.Lz)), z)
 
 
 @pytest.mark.parametrize("code", ["steane_code", "css_4_2_2_code", "css_6_2_2_code"])
@@ -174,14 +174,14 @@ def test_plus_state_gate_optimal(code: CSSCode, request) -> None:  # type: ignor
     assert not sp_circ_plus.zero_state
 
     circ_plus = sp_circ_plus.circ
-    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[arg-type]
+    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[operator]
 
     assert circ_plus.num_qubits == code.n
     assert circ_plus.num_nonlocal_gates() <= max_cnots
 
     x, z = get_stabs(circ_plus)
-    assert eq_span(code.Hz, z)  # type: ignore[arg-type]
-    assert eq_span(np.vstack((code.Hx, code.Lx)), x)  # type: ignore[arg-type]
+    assert eq_span(code.Hz, z)
+    assert eq_span(np.vstack((code.Hx, code.Lx)), x)
 
     sp_circ_zero = gate_optimal_prep_circuit(code, max_timeout=5, zero_state=True)
 
@@ -210,14 +210,14 @@ def test_plus_state_heuristic(code: CSSCode, request) -> None:  # type: ignore[n
     assert not sp_circ_plus.zero_state
 
     circ_plus = sp_circ_plus.circ
-    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[arg-type]
+    max_cnots = np.sum(code.Hx) + np.sum(code.Hz)  # type: ignore[operator]
 
     assert circ_plus.num_qubits == code.n
     assert circ_plus.num_nonlocal_gates() <= max_cnots
 
     x, z = get_stabs(circ_plus)
-    assert eq_span(code.Hz, z)  # type: ignore[arg-type]
-    assert eq_span(np.vstack((code.Hx, code.Lx)), x)  # type: ignore[arg-type]
+    assert eq_span(code.Hz, z)
+    assert eq_span(np.vstack((code.Hx, code.Lx)), x)
 
     sp_circ_zero = heuristic_prep_circuit(code, zero_state=True)
     circ_zero = sp_circ_zero.circ
@@ -378,7 +378,7 @@ def test_not_full_ft_opt_cc5(color_code_d5_sp: StatePrepCircuit) -> None:
     assert len(non_detected) == 0
 
     # Check that circuit is correct
-    n_cnots = np.sum(ver_stabs_1) + np.sum(ver_stabs_2)
+    n_cnots = np.sum(ver_stabs_1) + np.sum(ver_stabs_2)  # type: ignore[operator]
     circ_ver = gate_optimal_verification_circuit(circ, max_ancillas=3, max_timeout=5, full_fault_tolerance=True)
     assert circ_ver.num_qubits > circ.num_qubits + 5  # overhead from the flags
     assert circ_ver.num_nonlocal_gates() > n_cnots + circ.circ.num_nonlocal_gates()  # Overhead from Flag CNOTS
@@ -412,7 +412,7 @@ def test_not_full_ft_heuristic_cc5(color_code_d5_sp: StatePrepCircuit) -> None:
 
     # Check that circuit is correct
     circ_ver = heuristic_verification_circuit(circ, full_fault_tolerance=False)
-    n_cnots = np.sum(ver_stabs_1) + np.sum(ver_stabs_2)
+    n_cnots = np.sum(ver_stabs_1) + np.sum(ver_stabs_2)  # type: ignore[operator]
     assert circ_ver.num_qubits == circ.num_qubits + len(ver_stabs_1) + len(ver_stabs_2)
     assert circ_ver.num_nonlocal_gates() == n_cnots + circ.circ.num_nonlocal_gates()
 
diff --git a/test/test_ecc_functionality.cpp b/test/test_ecc_functionality.cpp
index a20148a1..819639fe 100644
--- a/test/test_ecc_functionality.cpp
+++ b/test/test_ecc_functionality.cpp
@@ -1,5 +1,4 @@
 #include "Definitions.hpp"
-#include "QuantumComputation.hpp"
 #include "dd/Package.hpp"
 #include "dd/Simulation.hpp"
 #include "ecc/Id.hpp"
@@ -9,8 +8,9 @@
 #include "ecc/Q7Steane.hpp"
 #include "ecc/Q9Shor.hpp"
 #include "ecc/Q9Surface.hpp"
-#include "operations/NonUnitaryOperation.hpp"
-#include "operations/OpType.hpp"
+#include "ir/QuantumComputation.hpp"
+#include "ir/operations/NonUnitaryOperation.hpp"
+#include "ir/operations/OpType.hpp"
 
 #include <algorithm>
 #include <cstddef>
diff --git a/test/test_originalUfd.cpp b/test/test_originalUfd.cpp
index bb4373b3..7a6604c4 100644
--- a/test/test_originalUfd.cpp
+++ b/test/test_originalUfd.cpp
@@ -6,8 +6,12 @@
 
 #include "Codes.hpp"
 #include "UFDecoder.hpp"
+#include "Utils.hpp"
 
+#include <cstddef>
 #include <gtest/gtest.h>
+#include <iostream>
+#include <vector>
 
 class OriginalUFDtest : public testing::TestWithParam<std::vector<bool>> {};
 class UniquelyCorrectableErrTestOriginal : public OriginalUFDtest {};
@@ -41,8 +45,8 @@ TEST_P(UniquelyCorrectableErrTestOriginal, SteaneCodeDecodingTestEstim) {
     auto      code = SteaneXCode();
     UFDecoder decoder;
     decoder.setCode(code);
-    std::cout << "code: " << std::endl
-              << code << std::endl;
+    std::cout << "code:\n"
+              << code << '\n';
     const std::vector<bool> err = GetParam();
 
     auto syndr = code.getXSyndrome(err);
@@ -53,23 +57,20 @@ TEST_P(UniquelyCorrectableErrTestOriginal, SteaneCodeDecodingTestEstim) {
     const auto& estim          = decodingResult.estimBoolVector;
     const auto& estimIdx       = decodingResult.estimNodeIdxVector;
     gf2Vec      estim2(err.size());
-    std::cout << "estiIdxs: ";
+    std::cout << "\nestiIdxs: ";
     for (auto idx : estimIdx) {
         estim2.at(idx) = true;
         std::cout << idx;
     }
-    std::cout << std::endl;
     const gf2Vec sol = GetParam();
 
-    std::cout << "Estim: " << std::endl;
+    std::cout << "\nEstim:\n";
     Utils::printGF2vector(estim);
-    std::cout << std::endl;
-    std::cout << "EstimIdx: ";
+    std::cout << "\nEstimIdx: ";
     Utils::printGF2vector(estim2);
-    std::cout << std::endl;
-    std::cout << "Sol: ";
+    std::cout << "\nSol: ";
     Utils::printGF2vector(sol);
-    std::cout << std::endl;
+    std::cout << '\n';
     EXPECT_TRUE(sol == estim);
     EXPECT_TRUE(sol == estim2);
 }
@@ -81,8 +82,8 @@ TEST_P(InCorrectableErrTestOriginal, SteaneCodeDecodingTestEstim) {
     auto      code = SteaneXCode();
     UFDecoder decoder;
     decoder.setCode(code);
-    std::cout << "code: " << std::endl
-              << code << std::endl;
+    std::cout << "code:\n"
+              << code << '\n';
     std::vector<bool> err = GetParam();
 
     auto syndr = code.getXSyndrome(err);
@@ -91,7 +92,7 @@ TEST_P(InCorrectableErrTestOriginal, SteaneCodeDecodingTestEstim) {
     const auto& estim          = decodingResult.estimBoolVector;
     const auto& estimIdx       = decodingResult.estimNodeIdxVector;
     gf2Vec      estim2(err.size());
-    std::cout << "estiIdxs: ";
+    std::cout << "\nestiIdxs: ";
     for (auto idx : estimIdx) {
         estim2.at(idx) = true;
         std::cout << idx;
@@ -103,11 +104,11 @@ TEST_P(InCorrectableErrTestOriginal, SteaneCodeDecodingTestEstim) {
 
     const gf2Vec sol = GetParam();
 
-    std::cout << "Estim: " << std::endl;
+    std::cout << "Estim: \n";
     Utils::printGF2vector(estim);
-    std::cout << "EstimIdx: " << std::endl;
+    std::cout << "EstimIdx: \n";
     Utils::printGF2vector(estim2);
-    std::cout << "Sol: " << std::endl;
+    std::cout << "Sol: \n";
     Utils::printGF2vector(sol);
     EXPECT_FALSE(sol == estim);
     EXPECT_FALSE(sol == estim2);
@@ -120,10 +121,10 @@ TEST_P(UpToStabCorrectableErrTestOriginal, SteaneCodeDecodingTest) {
     auto      code = SteaneCode();
     UFDecoder decoder;
     decoder.setCode(code);
-    std::cout << "code: " << std::endl
-              << code << std::endl;
+    std::cout << "code:\n"
+              << code << '\n';
     std::vector<bool> err = GetParam();
-    std::cout << "err :" << std::endl;
+    std::cout << "err:\n";
     Utils::printGF2vector(err);
     auto syndr = code.getXSyndrome(err);
     decoder.decode(syndr);
@@ -131,12 +132,12 @@ TEST_P(UpToStabCorrectableErrTestOriginal, SteaneCodeDecodingTest) {
     const auto& estim          = decodingResult.estimBoolVector;
     const auto& estimIdx       = decodingResult.estimNodeIdxVector;
     gf2Vec      estim2(err.size());
-    std::cout << "estiIdxs: ";
+    std::cout << "\nestiIdxs: ";
     for (auto idx : estimIdx) {
         estim2.at(idx) = true;
         std::cout << idx;
     }
-    std::cout << std::endl;
+    std::cout << '\n';
     std::vector<bool> residualErr(err.size());
     for (size_t i = 0; i < err.size(); i++) {
         residualErr.at(i) = (err[i] != estim[i]);